Announcements

This commit is contained in:
Thomas 2022-06-16 18:30:16 +02:00
parent 5d9268653c
commit 890b9b64a6
23 changed files with 1322 additions and 15 deletions

View file

@ -150,6 +150,9 @@
<activity <activity
android:name=".activities.HashTagActivity" android:name=".activities.HashTagActivity"
android:configChanges="keyboardHidden|orientation|screenSize" /> android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.AnnouncementActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity <activity
android:name=".activities.MediaActivity" android:name=".activities.MediaActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"

View file

@ -80,6 +80,7 @@ import java.util.regex.Pattern;
import app.fedilab.android.activities.AboutActivity; import app.fedilab.android.activities.AboutActivity;
import app.fedilab.android.activities.ActionActivity; import app.fedilab.android.activities.ActionActivity;
import app.fedilab.android.activities.AdminActionActivity; import app.fedilab.android.activities.AdminActionActivity;
import app.fedilab.android.activities.AnnouncementActivity;
import app.fedilab.android.activities.BaseActivity; import app.fedilab.android.activities.BaseActivity;
import app.fedilab.android.activities.ComposeActivity; import app.fedilab.android.activities.ComposeActivity;
import app.fedilab.android.activities.ContextActivity; import app.fedilab.android.activities.ContextActivity;
@ -99,6 +100,8 @@ import app.fedilab.android.activities.ScheduledActivity;
import app.fedilab.android.activities.SearchResultTabActivity; import app.fedilab.android.activities.SearchResultTabActivity;
import app.fedilab.android.activities.SettingsActivity; import app.fedilab.android.activities.SettingsActivity;
import app.fedilab.android.broadcastreceiver.NetworkStateReceiver; import app.fedilab.android.broadcastreceiver.NetworkStateReceiver;
import app.fedilab.android.client.entities.api.Emoji;
import app.fedilab.android.client.entities.api.EmojiInstance;
import app.fedilab.android.client.entities.api.Filter; import app.fedilab.android.client.entities.api.Filter;
import app.fedilab.android.client.entities.api.Instance; import app.fedilab.android.client.entities.api.Instance;
import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.api.MastodonList;
@ -128,6 +131,7 @@ import es.dmoral.toasty.Toasty;
public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener { public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener {
public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software; public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software;
public static List<Emoji> emojis;
public static Account.API api; public static Account.API api;
public static boolean admin; public static boolean admin;
public static WeakReference<Account> accountWeakReference; public static WeakReference<Account> accountWeakReference;
@ -351,6 +355,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
} else if (id == R.id.nav_partnership) { } else if (id == R.id.nav_partnership) {
Intent intent = new Intent(this, PartnerShipActivity.class); Intent intent = new Intent(this, PartnerShipActivity.class);
startActivity(intent); startActivity(intent);
} else if (id == R.id.nav_announcements) {
Intent intent = new Intent(this, AnnouncementActivity.class);
startActivity(intent);
} }
binding.drawerLayout.close(); binding.drawerLayout.close();
return false; return false;
@ -724,6 +731,15 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE)); binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE));
//For receiving data from other activities //For receiving data from other activities
LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA)); LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA));
if (emojis == null) {
new Thread(() -> {
try {
emojis = new EmojiInstance(BaseMainActivity.this).getEmojiList(BaseMainActivity.currentInstance);
} catch (DBException e) {
e.printStackTrace();
}
}).start();
}
} }
@ -856,6 +872,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
return false; return false;
}); });
popup.show(); popup.show();
} }
public void refreshFragment() { public void refreshFragment() {

View file

@ -0,0 +1,82 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program 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.
*
* Fedilab 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 Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.BaseMainActivity.emojis;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import androidx.core.content.ContextCompat;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.EmojiInstance;
import app.fedilab.android.databinding.ActivityAnnouncementBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAnnouncement;
public class AnnouncementActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
ActivityAnnouncementBinding binding = ActivityAnnouncementBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary)));
}
binding.title.setText(R.string.action_announcements);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_tags, new FragmentMastodonAnnouncement(), null, null, null);
if (emojis == null) {
new Thread(() -> {
try {
emojis = new EmojiInstance(AnnouncementActivity.this).getEmojiList(BaseMainActivity.currentInstance);
} catch (DBException e) {
e.printStackTrace();
}
}).start();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -30,28 +30,27 @@ import retrofit2.http.Query;
public interface MastodonAnnouncementsService { public interface MastodonAnnouncementsService {
@GET("/announcements") @GET("announcements")
Call<List<Announcement>> getAnnouncements( Call<List<Announcement>> getAnnouncements(
@Header("Authorization") String token, @Header("Authorization") String token,
@Query("with_dismissed") boolean with_dismissed @Query("with_dismissed") Boolean with_dismissed
); );
@FormUrlEncoded @FormUrlEncoded
@POST("/announcements/{id}/dismiss") @POST("announcements/{id}/dismiss")
Call<Void> dismiss( Call<Void> dismiss(
@Header("Authorization") String app_token, @Header("Authorization") String app_token,
@Path("id") String id @Path("id") String id
); );
@FormUrlEncoded @PUT("announcements/{id}/reactions/{name}")
@PUT("/announcements/{id}/reactions/{name}")
Call<Void> addReaction( Call<Void> addReaction(
@Header("Authorization") String app_token, @Header("Authorization") String app_token,
@Path("id") String id, @Path("id") String id,
@Path("name") String name @Path("name") String name
); );
@DELETE("/announcements/{id}/reactions/{name}") @DELETE("announcements/{id}/reactions/{name}")
Call<Void> removeReaction( Call<Void> removeReaction(
@Header("Authorization") String app_token, @Header("Authorization") String app_token,
@Path("id") String id, @Path("id") String id,

View file

@ -14,6 +14,8 @@ package app.fedilab.android.client.entities.api;
* You should have received a copy of the GNU General Public License along with Fedilab; if not, * You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
import android.text.Spannable;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import java.util.Date; import java.util.Date;
@ -46,4 +48,7 @@ public class Announcement {
public List<Emoji> emojis; public List<Emoji> emojis;
@SerializedName("reactions") @SerializedName("reactions")
public List<Reaction> reactions; public List<Reaction> reactions;
//Some extra spannable element - They will be filled automatically when fetching the status
public transient Spannable span_content;
} }

View file

@ -602,7 +602,6 @@ public class Helper {
public static void removeAccount(Activity activity, Account account) throws DBException { public static void removeAccount(Activity activity, Account account) throws DBException {
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity);
//Current user //Current user
SQLiteDatabase db = Sqlite.getInstance(activity.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
String userId = sharedpreferences.getString(PREF_USER_ID, null); String userId = sharedpreferences.getString(PREF_USER_ID, null);
String instance = sharedpreferences.getString(PREF_USER_INSTANCE, null); String instance = sharedpreferences.getString(PREF_USER_INSTANCE, null);
Account accountDB = new Account(activity); Account accountDB = new Account(activity);
@ -881,6 +880,34 @@ public class Helper {
return Cyanea.getInstance().isDark() ? R.style.PopupDark : R.style.Popup; return Cyanea.getInstance().isDark() ? R.style.PopupDark : R.style.Popup;
} }
/**
* Load a media into a view
*
* @param view ImageView - the view where the image will be loaded
* @param url - String
*/
public static void loadImage(ImageView view, String url) {
Context context = view.getContext();
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false);
if (disableGif || (!url.endsWith(".gif"))) {
Glide.with(view.getContext())
.asDrawable()
.load(url)
.thumbnail(0.1f)
.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10)))
.into(view);
} else {
Glide.with(view.getContext())
.asGif()
.load(url)
.thumbnail(0.1f)
.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10)))
.into(view);
}
}
/** /**
* Load a profile picture for the account * Load a profile picture for the account
* *

View file

@ -76,6 +76,7 @@ import app.fedilab.android.activities.HashTagActivity;
import app.fedilab.android.activities.MainActivity; import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.activities.ProfileActivity; import app.fedilab.android.activities.ProfileActivity;
import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.api.Account;
import app.fedilab.android.client.entities.api.Announcement;
import app.fedilab.android.client.entities.api.Attachment; import app.fedilab.android.client.entities.api.Attachment;
import app.fedilab.android.client.entities.api.Emoji; import app.fedilab.android.client.entities.api.Emoji;
import app.fedilab.android.client.entities.api.Field; import app.fedilab.android.client.entities.api.Field;
@ -553,6 +554,426 @@ public class SpannableHelper {
} }
/**
* Convert HTML content to text. Also, it handles click on link and transform emoji
* This needs to be run asynchronously
*
* @param context {@link Context}
* @param announcement {@link Announcement} - Announcement concerned by the spannable transformation
* @param text String - text to convert, it can be content, spoiler, poll items, etc.
* @return Spannable string
*/
private static Spannable convert(@NonNull Context context, @NonNull Announcement announcement, String text) {
SpannableString initialContent;
if (text == null) {
return null;
}
Matcher matcherALink = Helper.aLink.matcher(text);
//We stock details
HashMap<String, String> urlDetails = new HashMap<>();
while (matcherALink.find()) {
String urlText = matcherALink.group(3);
String url = matcherALink.group(2);
if (urlText != null) {
urlText = urlText.substring(1);
}
if (url != null && urlText != null && !url.equals(urlText) && !urlText.contains("<span")) {
urlDetails.put(url, urlText);
text = text.replaceAll(Pattern.quote(matcherALink.group()), Matcher.quoteReplacement(url));
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY));
else
initialContent = new SpannableString(Html.fromHtml(text));
SpannableStringBuilder content = new SpannableStringBuilder(initialContent);
URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class);
for (URLSpan span : urls) {
content.removeSpan(span);
}
//--- EMOJI ----
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false);
List<Emoji> emojiList = announcement.emojis;
//Will convert emoji if asked
if (emojiList != null && emojiList.size() > 0) {
for (Emoji emoji : emojiList) {
if (Helper.isValidContextForGlide(context)) {
FutureTarget<File> futureTarget = Glide.with(context)
.asFile()
.load(disableGif ? emoji.static_url : emoji.url)
.submit();
try {
File file = futureTarget.get();
final String targetedEmoji = ":" + emoji.shortcode + ":";
if (content.toString().contains(targetedEmoji)) {
//emojis can be used several times so we have to loop
for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) {
final int endPosition = startPosition + targetedEmoji.length();
if (endPosition <= content.toString().length() && endPosition >= startPosition) {
ImageSpan imageSpan;
if (APNGParser.isAPNG(file.getAbsolutePath())) {
APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath());
try {
apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
apngDrawable.setVisible(true, true);
imageSpan = new ImageSpan(apngDrawable);
if (endPosition <= content.length()) {
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
} catch (Exception ignored) {
}
} else if (GifParser.isGif(file.getAbsolutePath())) {
GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath());
try {
gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
gifDrawable.setVisible(true, true);
imageSpan = new ImageSpan(gifDrawable);
if (endPosition <= content.length()) {
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
} catch (Exception ignored) {
}
} else {
Drawable drawable = Drawable.createFromPath(file.getAbsolutePath());
try {
drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
drawable.setVisible(true, true);
imageSpan = new ImageSpan(drawable);
if (endPosition <= content.length()) {
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
} catch (Exception ignored) {
}
}
}
}
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
//--- URLs ----
Matcher matcherLink = Patterns.WEB_URL.matcher(content);
int offSetTruncate = 0;
while (matcherLink.find()) {
int matchStart = matcherLink.start() - offSetTruncate;
int matchEnd = matchStart + matcherLink.group().length();
if (matchEnd > content.toString().length()) {
matchEnd = content.toString().length();
}
if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) {
continue;
}
final String url = content.toString().substring(matchStart, matchEnd);
String newURL = Helper.transformURL(context, url);
//If URL has been transformed
if (newURL.compareTo(url) != 0) {
content.replace(matchStart, matchEnd, newURL);
offSetTruncate += (newURL.length() - url.length());
matchEnd = matchStart + newURL.length();
//The transformed URL was in the list of URLs having a different names
if (urlDetails.containsKey(url)) {
urlDetails.put(newURL, urlDetails.get(url));
}
}
//Truncate URL if needed
//TODO: add an option to disable truncated URLs
String urlText = newURL;
if (newURL.length() > 30 && !urlDetails.containsKey(urlText)) {
urlText = urlText.substring(0, 30);
urlText += "";
content.replace(matchStart, matchEnd, urlText);
matchEnd = matchStart + 31;
offSetTruncate += (newURL.length() - urlText.length());
} else if (urlDetails.containsKey(urlText) && urlDetails.get(urlText) != null) {
urlText = urlDetails.get(urlText);
if (urlText != null) {
content.replace(matchStart, matchEnd, urlText);
matchEnd = matchStart + urlText.length();
offSetTruncate += (newURL.length() - urlText.length());
}
}
if (matchEnd <= content.length() && matchEnd >= matchStart) {
content.setSpan(new LongClickableSpan() {
@Override
public void onLongClick(View view) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle());
PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context));
dialogBuilder.setView(popupLinksBinding.getRoot());
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.show();
popupLinksBinding.displayFullLink.setOnClickListener(v -> {
AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
builder.setMessage(url);
builder.setTitle(context.getString(R.string.display_full_link));
builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
.show();
alertDialog.dismiss();
});
popupLinksBinding.shareLink.setOnClickListener(v -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
sendIntent.putExtra(Intent.EXTRA_TEXT, url);
sendIntent.setType("text/plain");
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with));
intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intentChooser);
alertDialog.dismiss();
});
popupLinksBinding.openOtherApp.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
} catch (Exception e) {
Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show();
}
alertDialog.dismiss();
});
popupLinksBinding.copyLink.setOnClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, url);
if (clipboard != null) {
clipboard.setPrimaryClip(clip);
Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
}
alertDialog.dismiss();
});
popupLinksBinding.checkRedirect.setOnClickListener(v -> {
try {
URL finalUrlCheck = new URL(url);
new Thread(() -> {
try {
String redirect = null;
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection();
httpsURLConnection.setConnectTimeout(10 * 1000);
httpsURLConnection.setRequestProperty("http.keepAlive", "false");
httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT);
httpsURLConnection.setRequestMethod("HEAD");
if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) {
Map<String, List<String>> map = httpsURLConnection.getHeaderFields();
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
if (entry.toString().toLowerCase().startsWith("location")) {
Matcher matcher = urlPattern.matcher(entry.toString());
if (matcher.find()) {
redirect = matcher.group(1);
}
}
}
}
httpsURLConnection.getInputStream().close();
if (redirect != null && redirect.compareTo(url) != 0) {
URL redirectURL = new URL(redirect);
String host = redirectURL.getHost();
String protocol = redirectURL.getProtocol();
if (protocol == null || host == null) {
redirect = null;
}
}
Handler mainHandler = new Handler(context.getMainLooper());
String finalRedirect = redirect;
Runnable myRunnable = () -> {
AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle());
if (finalRedirect != null) {
builder1.setMessage(context.getString(R.string.redirect_detected, url, finalRedirect));
builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> {
ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect);
if (clipboard1 != null) {
clipboard1.setPrimaryClip(clip1);
Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
}
dialog.dismiss();
});
builder1.setNeutralButton(R.string.share_link, (dialog, which) -> {
Intent sendIntent1 = new Intent(Intent.ACTION_SEND);
sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
sendIntent1.putExtra(Intent.EXTRA_TEXT, url);
sendIntent1.setType("text/plain");
context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with)));
dialog.dismiss();
});
} else {
builder1.setMessage(R.string.no_redirect);
}
builder1.setTitle(context.getString(R.string.check_redirect));
builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
.show();
};
mainHandler.post(myRunnable);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
} catch (MalformedURLException e) {
e.printStackTrace();
}
alertDialog.dismiss();
});
}
@Override
public void onClick(@NonNull View textView) {
textView.setTag(CLICKABLE_SPAN);
Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
Matcher matcherLink = link.matcher(url);
if (matcherLink.find() && !url.contains("medium.com")) {
if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot
CrossActionHelper.fetchRemoteStatus(context, MainActivity.accountWeakReference.get(), url, new CrossActionHelper.Callback() {
@Override
public void federatedStatus(Status status) {
Intent intent = new Intent(context, ContextActivity.class);
intent.putExtra(Helper.ARG_STATUS, status);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
@Override
public void federatedAccount(Account account) {
}
});
}
} else {
Helper.openBrowser(context, newURL);
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(linkColor);
}
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
// --- For all patterns defined in Helper class ---
for (Map.Entry<Helper.PatternType, Pattern> entry : Helper.patternHashMap.entrySet()) {
Helper.PatternType patternType = entry.getKey();
Pattern pattern = entry.getValue();
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
int matchStart = matcher.start();
int matchEnd = matcher.end();
String word = content.toString().substring(matchStart, matchEnd);
if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) {
URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class);
content.removeSpan(span);
content.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View textView) {
textView.setTag(CLICKABLE_SPAN);
switch (patternType) {
case TAG:
Intent intent = new Intent(context, HashTagActivity.class);
Bundle b = new Bundle();
b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim());
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
case GROUP:
break;
case MENTION:
intent = new Intent(context, ProfileActivity.class);
b = new Bundle();
Mention targetedMention = null;
HashMap<String, Integer> countUsername = new HashMap<>();
for (Mention mention : announcement.mentions) {
Integer count = countUsername.get(mention.username);
if (count == null) {
count = 0;
}
if (countUsername.containsKey(mention.username)) {
countUsername.put(mention.username, count + 1);
} else {
countUsername.put(mention.username, 1);
}
}
for (Mention mention : announcement.mentions) {
Integer count = countUsername.get(mention.username);
if (count == null) {
count = 0;
}
if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) {
targetedMention = mention;
break;
}
}
if (targetedMention != null) {
b.putString(Helper.ARG_USER_ID, targetedMention.id);
} else {
b.putString(Helper.ARG_MENTION, word.trim());
}
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
case MENTION_LONG:
intent = new Intent(context, ProfileActivity.class);
b = new Bundle();
targetedMention = null;
for (Mention mention : announcement.mentions) {
if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) {
targetedMention = mention;
break;
}
}
if (targetedMention != null) {
b.putString(Helper.ARG_USER_ID, targetedMention.id);
} else {
b.putString(Helper.ARG_MENTION, word.trim());
}
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(linkColor);
}
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
}
return trimSpannable(new SpannableStringBuilder(content));
}
/** /**
* Remove extra carriage returns at the bottom due to <p> tags in toots * Remove extra carriage returns at the bottom due to <p> tags in toots
* *
@ -586,6 +1007,24 @@ public class SpannableHelper {
return statuses; return statuses;
} }
public static List<Announcement> convertAnnouncement(Context context, List<Announcement> announcements) {
if (announcements != null) {
for (Announcement announcement : announcements) {
convertAnnouncement(context, announcement);
}
}
return announcements;
}
public static Announcement convertAnnouncement(Context context, Announcement announcement) {
if (announcement != null) {
announcement.span_content = SpannableHelper.convert(context, announcement, announcement.content);
}
return announcement;
}
public static Status convertStatus(Context context, Status status) { public static Status convertStatus(Context context, Status status) {
if (status != null) { if (status != null) {
status.span_content = SpannableHelper.convert(context, status, status.content); status.span_content = SpannableHelper.convert(context, status, status.content);

View file

@ -0,0 +1,221 @@
package app.fedilab.android.ui.drawer;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program 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.
*
* Fedilab 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 Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static android.content.Context.INPUT_METHOD_SERVICE;
import static app.fedilab.android.BaseMainActivity.emojis;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.GridView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.vanniktech.emoji.EmojiManager;
import com.vanniktech.emoji.EmojiPopup;
import com.vanniktech.emoji.one.EmojiOneProvider;
import java.util.List;
import app.fedilab.android.R;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.client.entities.api.Announcement;
import app.fedilab.android.client.entities.api.Reaction;
import app.fedilab.android.databinding.DrawerAnnouncementBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
public class AnnouncementAdapter extends RecyclerView.Adapter<AnnouncementAdapter.AnnouncementHolder> {
private final List<Announcement> announcements;
private Context context;
private AnnouncementsVM announcementsVM;
private AlertDialog alertDialogEmoji;
public AnnouncementAdapter(List<Announcement> announcements) {
this.announcements = announcements;
}
public int getCount() {
return announcements.size();
}
public Announcement getItem(int position) {
return announcements.get(position);
}
@NonNull
@Override
public AnnouncementHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
context = parent.getContext();
DrawerAnnouncementBinding itemBinding = DrawerAnnouncementBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new AnnouncementHolder(itemBinding);
}
@Override
public void onBindViewHolder(@NonNull AnnouncementHolder holder, int position) {
Announcement announcement = announcements.get(position);
if (announcement.reactions != null && announcement.reactions.size() > 0) {
ReactionAdapter reactionAdapter = new ReactionAdapter(announcement.id, announcement.reactions);
holder.binding.reactionsView.setAdapter(reactionAdapter);
LinearLayoutManager layoutManager
= new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
holder.binding.reactionsView.setLayoutManager(layoutManager);
} else {
holder.binding.reactionsView.setAdapter(null);
}
holder.binding.content.setText(announcement.span_content, TextView.BufferType.SPANNABLE);
if (announcement.starts_at != null) {
String dateIni;
String dateEnd;
if (announcement.all_day) {
dateIni = Helper.shortDateToString(announcement.starts_at);
dateEnd = Helper.shortDateToString(announcement.ends_at);
} else {
dateIni = Helper.longDateToString(announcement.starts_at);
dateEnd = Helper.longDateToString(announcement.ends_at);
}
String text = context.getString(R.string.action_announcement_from_to, dateIni, dateEnd);
holder.binding.dates.setText(text);
holder.binding.dates.setVisibility(View.VISIBLE);
} else {
holder.binding.dates.setVisibility(View.GONE);
}
holder.binding.statusEmoji.setOnClickListener(v -> {
EmojiManager.install(new EmojiOneProvider());
final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(holder.binding.statusEmoji).setOnEmojiPopupDismissListener(() -> {
InputMethodManager imm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(holder.binding.statusEmoji.getWindowToken(), 0);
}).setOnEmojiClickListener((emoji, imageView) -> {
String emojiStr = imageView.getUnicode();
boolean alreadyAdded = false;
for (Reaction reaction : announcement.reactions) {
if (reaction.name.compareTo(emojiStr) == 0) {
alreadyAdded = true;
reaction.count = (reaction.count - 1);
if (reaction.count == 0) {
announcement.reactions.remove(reaction);
}
notifyItemChanged(position);
break;
}
}
if (!alreadyAdded) {
Reaction reaction = new Reaction();
reaction.me = true;
reaction.count = 1;
reaction.name = emojiStr;
announcement.reactions.add(0, reaction);
notifyItemChanged(position);
}
announcementsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AnnouncementsVM.class);
if (alreadyAdded) {
announcementsVM.removeReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
} else {
announcementsVM.addReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
}
})
.build(holder.binding.fakeEdittext);
emojiPopup.toggle();
});
holder.binding.statusAddCustomEmoji.setOnClickListener(v -> {
final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
int paddingPixel = 15;
float density = context.getResources().getDisplayMetrics().density;
int paddingDp = (int) (paddingPixel * density);
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
builder.setTitle(R.string.insert_emoji);
if (emojis != null && emojis.size() > 0) {
GridView gridView = new GridView(context);
gridView.setAdapter(new EmojiAdapter(emojis));
gridView.setNumColumns(5);
gridView.setOnItemClickListener((parent, view, index, id) -> {
String emojiStr = emojis.get(index).shortcode;
String url = emojis.get(index).url;
String static_url = emojis.get(index).static_url;
boolean alreadyAdded = false;
for (Reaction reaction : announcement.reactions) {
if (reaction.name.compareTo(emojiStr) == 0) {
alreadyAdded = true;
reaction.count = (reaction.count - 1);
if (reaction.count == 0) {
announcement.reactions.remove(reaction);
}
notifyItemChanged(position);
break;
}
}
if (!alreadyAdded) {
Reaction reaction = new Reaction();
reaction.me = true;
reaction.count = 1;
reaction.name = emojiStr;
reaction.url = url;
reaction.static_url = static_url;
announcement.reactions.add(0, reaction);
notifyItemChanged(position);
}
announcementsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AnnouncementsVM.class);
if (alreadyAdded) {
announcementsVM.removeReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
} else {
announcementsVM.addReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
}
alertDialogEmoji.dismiss();
});
gridView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp);
builder.setView(gridView);
} else {
TextView textView = new TextView(context);
textView.setText(context.getString(R.string.no_emoji));
textView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp);
builder.setView(textView);
}
alertDialogEmoji = builder.show();
});
}
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return announcements.size();
}
static class AnnouncementHolder extends RecyclerView.ViewHolder {
DrawerAnnouncementBinding binding;
AnnouncementHolder(DrawerAnnouncementBinding itemView) {
super(itemView.getRoot());
binding = itemView;
}
}
}

View file

@ -15,6 +15,7 @@ package app.fedilab.android.ui.drawer;
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.BaseMainActivity.emojis;
import static app.fedilab.android.BaseMainActivity.instanceInfo; import static app.fedilab.android.BaseMainActivity.instanceInfo;
import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE; import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE;
@ -124,7 +125,6 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private final String visibility; private final String visibility;
private final app.fedilab.android.client.entities.api.Account mentionedAccount; private final app.fedilab.android.client.entities.api.Account mentionedAccount;
public ManageDrafts manageDrafts; public ManageDrafts manageDrafts;
List<Emoji> emojis;
private int statusCount; private int statusCount;
private Context context; private Context context;
private AlertDialog alertDialogEmoji; private AlertDialog alertDialogEmoji;
@ -1226,12 +1226,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private void displayEmojiPicker(ComposeViewHolder holder) throws DBException { private void displayEmojiPicker(ComposeViewHolder holder) throws DBException {
if (emojis != null) {
emojis.clear();
emojis = null;
}
emojis = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance);
final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
int paddingPixel = 15; int paddingPixel = 15;
float density = context.getResources().getDisplayMetrics().density; float density = context.getResources().getDisplayMetrics().density;

View file

@ -0,0 +1,115 @@
package app.fedilab.android.ui.drawer;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program 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.
*
* Fedilab 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 Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import app.fedilab.android.R;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.client.entities.api.Reaction;
import app.fedilab.android.databinding.DrawerReactionBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
/**
* Created by Thomas on 10/03/2020.
* Adapter for reactions on messages
*/
public class ReactionAdapter extends RecyclerView.Adapter<ReactionAdapter.ReactionHolder> {
private final List<Reaction> reactions;
private final String announcementId;
private Context context;
ReactionAdapter(String announcementId, List<Reaction> reactions) {
this.reactions = reactions;
this.announcementId = announcementId;
}
@NonNull
@Override
public ReactionHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
context = parent.getContext();
DrawerReactionBinding itemBinding = DrawerReactionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ReactionHolder(itemBinding);
}
@Override
public void onBindViewHolder(@NonNull ReactionHolder holder, int position) {
final Reaction reaction = reactions.get(position);
holder.binding.reactionCount.setText(String.valueOf(reaction.count));
if (reaction.me) {
holder.binding.reactionContainer.setBackgroundResource(R.drawable.reaction_voted);
} else {
holder.binding.reactionContainer.setBackgroundResource(R.drawable.reaction_border);
}
if (reaction.url != null) {
holder.binding.reactionName.setVisibility(View.GONE);
holder.binding.reactionEmoji.setVisibility(View.VISIBLE);
holder.binding.reactionEmoji.setContentDescription(reaction.name);
Helper.loadImage(holder.binding.reactionEmoji, reaction.url);
} else {
holder.binding.reactionName.setText(reaction.name);
holder.binding.reactionName.setVisibility(View.VISIBLE);
holder.binding.reactionEmoji.setVisibility(View.GONE);
}
AnnouncementsVM announcementsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AnnouncementsVM.class);
holder.binding.reactionContainer.setOnClickListener(v -> {
if (reaction.me) {
announcementsVM.removeReaction(MainActivity.currentInstance, MainActivity.currentToken, announcementId, reaction.name);
reaction.me = false;
} else {
announcementsVM.addReaction(MainActivity.currentInstance, MainActivity.currentToken, announcementId, reaction.name);
reaction.me = true;
}
notifyItemChanged(position);
});
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return reactions.size();
}
static class ReactionHolder extends RecyclerView.ViewHolder {
DrawerReactionBinding binding;
ReactionHolder(DrawerReactionBinding itemView) {
super(itemView.getRoot());
binding = itemView;
}
}
}

View file

@ -0,0 +1,121 @@
package app.fedilab.android.ui.fragment.timeline;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program 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.
*
* Fedilab 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 Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.Announcement;
import app.fedilab.android.databinding.FragmentPaginationBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.AnnouncementAdapter;
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
public class FragmentMastodonAnnouncement extends Fragment {
private FragmentPaginationBinding binding;
private AnnouncementsVM announcementsVM;
private AnnouncementAdapter announcementAdapter;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentPaginationBinding.inflate(inflater, container, false);
View root = binding.getRoot();
binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity()));
int c1 = getResources().getColor(R.color.cyanea_accent_reference);
binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference));
binding.swipeContainer.setColorSchemeColors(
c1, c1, c1
);
announcementsVM = new ViewModelProvider(FragmentMastodonAnnouncement.this).get(AnnouncementsVM.class);
binding.loader.setVisibility(View.VISIBLE);
binding.recyclerView.setVisibility(View.GONE);
announcementsVM.getAnnouncements(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
.observe(getViewLifecycleOwner(), this::initializeAnnouncementView);
return root;
}
@Override
public void onResume() {
super.onResume();
NotificationManager notificationManager = (NotificationManager) requireActivity().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Helper.NOTIFICATION_USER_NOTIF);
}
/**
* Intialize the view for announcements
*
* @param announcements List of {@link Announcement}
*/
private void initializeAnnouncementView(List<Announcement> announcements) {
binding.loader.setVisibility(View.GONE);
binding.swipeContainer.setRefreshing(false);
if (announcements == null || announcements.size() == 0) {
binding.noActionText.setText(R.string.no_announcements);
binding.noAction.setVisibility(View.VISIBLE);
binding.recyclerView.setVisibility(View.GONE);
return;
} else {
binding.noAction.setVisibility(View.GONE);
binding.recyclerView.setVisibility(View.VISIBLE);
}
announcementAdapter = new AnnouncementAdapter(announcements);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(announcementAdapter);
binding.swipeContainer.setOnRefreshListener(() -> {
binding.swipeContainer.setRefreshing(true);
announcementsVM.getAnnouncements(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
.observe(getViewLifecycleOwner(), this::initializeAnnouncementView);
});
}
public void scrollToTop() {
binding.recyclerView.scrollToPosition(0);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding.recyclerView.setAdapter(null);
announcementAdapter = null;
binding = null;
}
}

View file

@ -58,6 +58,7 @@ import app.fedilab.android.helper.SpannableHelper;
import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.StatusAdapter; import app.fedilab.android.ui.drawer.StatusAdapter;
import app.fedilab.android.viewmodel.mastodon.AccountsVM; import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
import app.fedilab.android.viewmodel.mastodon.SearchVM; import app.fedilab.android.viewmodel.mastodon.SearchVM;
import app.fedilab.android.viewmodel.mastodon.TimelinesVM; import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
@ -70,6 +71,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
private FragmentPaginationBinding binding; private FragmentPaginationBinding binding;
private TimelinesVM timelinesVM; private TimelinesVM timelinesVM;
private AccountsVM accountsVM; private AccountsVM accountsVM;
private AnnouncementsVM announcementsVM;
private boolean flagLoading; private boolean flagLoading;
private List<Status> statuses; private List<Status> statuses;
private String search, searchCache; private String search, searchCache;
@ -237,6 +239,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class); timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class);
accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class); accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class);
announcementsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AnnouncementsVM.class);
binding.loader.setVisibility(View.VISIBLE); binding.loader.setVisibility(View.VISIBLE);
binding.recyclerView.setVisibility(View.GONE); binding.recyclerView.setVisibility(View.GONE);

View file

@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
import app.fedilab.android.client.endpoints.MastodonAnnouncementsService; import app.fedilab.android.client.endpoints.MastodonAnnouncementsService;
import app.fedilab.android.client.entities.api.Announcement; import app.fedilab.android.client.entities.api.Announcement;
import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.SpannableHelper;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Response; import retrofit2.Response;
@ -67,7 +68,7 @@ public class AnnouncementsVM extends AndroidViewModel {
* @param withDismissed If true, response will include announcements dismissed by the user. Defaults to false. * @param withDismissed If true, response will include announcements dismissed by the user. Defaults to false.
* @return {@link LiveData} containing a {@link List} of {@link Announcement}s * @return {@link LiveData} containing a {@link List} of {@link Announcement}s
*/ */
public LiveData<List<Announcement>> getAnnouncements(@NonNull String instance, String token, boolean withDismissed) { public LiveData<List<Announcement>> getAnnouncements(@NonNull String instance, String token, Boolean withDismissed) {
MastodonAnnouncementsService mastodonAnnouncementsService = init(instance); MastodonAnnouncementsService mastodonAnnouncementsService = init(instance);
announcementListMutableLiveData = new MutableLiveData<>(); announcementListMutableLiveData = new MutableLiveData<>();
new Thread(() -> { new Thread(() -> {
@ -78,6 +79,7 @@ public class AnnouncementsVM extends AndroidViewModel {
Response<List<Announcement>> getAnnouncementsResponse = getAnnouncementsCall.execute(); Response<List<Announcement>> getAnnouncementsResponse = getAnnouncementsCall.execute();
if (getAnnouncementsResponse.isSuccessful()) { if (getAnnouncementsResponse.isSuccessful()) {
announcementList = getAnnouncementsResponse.body(); announcementList = getAnnouncementsResponse.body();
SpannableHelper.convertAnnouncement(getApplication(), announcementList);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,9V7h-2V2.84C14.77,2.3 13.42,2 11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12c0,-1.05 -0.17,-2.05 -0.47,-3H18zM15.5,8C16.33,8 17,8.67 17,9.5S16.33,11 15.5,11S14,10.33 14,9.5S14.67,8 15.5,8zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,17.5c-2.33,0 -4.31,-1.46 -5.11,-3.5h10.22C16.31,16.04 14.33,17.5 12,17.5zM22,3h2v2h-2v2h-2V5h-2V3h2V1h2V3z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12c0,5.52 4.47,10 9.99,10C17.52,22 22,17.52 22,12C22,6.48 17.52,2 11.99,2zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,18c-2.28,0 -4.22,-1.66 -5,-4h10C16.22,16.34 14.28,18 12,18zM15.5,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S14.67,8 15.5,8S17,8.67 17,9.5S16.33,11 15.5,11z" />
</vector>

View file

@ -2,7 +2,7 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:tint="#FFFFFF"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="1dp"
android:color="?colorAccent" />
<corners android:radius="10dp" />
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorAccent" />
<stroke
android:width="1dp"
android:color="?colorAccent" />
<corners android:radius="10dp" />
</shape>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2022 Thomas Schneider
This file is a part of Fedilab
This program 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.
Fedilab 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 Fedilab; if not,
see <http://www.gnu.org/licenses>
-->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/cyanea_primary_reference">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|enterAlways">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_tags"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2022 Thomas Schneider
This file is a part of Fedilab
This program 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.
Fedilab 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 Fedilab; if not,
see <http://www.gnu.org/licenses>
-->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cardview_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/card_margin"
android:layout_marginTop="@dimen/card_margin"
android:clipChildren="false"
android:clipToPadding="false"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_status_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/card_margin"
android:layout_marginTop="@dimen/card_margin"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dates"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<app.fedilab.android.helper.CustomTextView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="6dp"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dates"
tools:maxLines="10"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/status_reactions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/reactions_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/status_add_custom_emoji"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="5dp"
android:contentDescription="@string/add_reaction"
android:src="@drawable/ic_baseline_emoji_emotions_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/iconColor" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/status_emoji"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="5dp"
android:contentDescription="@string/add_reaction"
android:src="@drawable/ic_baseline_add_reaction_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/iconColor" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<app.fedilab.android.helper.FedilabAutoCompleteTextView
android:id="@+id/fake_edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
android:inputType="text" />
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2022 Thomas Schneider
This file is a part of Fedilab
This program 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.
Fedilab 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 Fedilab; if not,
see <http://www.gnu.org/licenses>.
-->
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/reaction_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="5dp"
android:paddingLeft="5dp"
android:paddingTop="2dp"
android:paddingEnd="5dp"
android:paddingRight="5dp"
android:paddingBottom="2dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/reaction_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="3dp"
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/reaction_emoji"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="3dp"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/reaction_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp" />
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -28,6 +28,13 @@
android:id="@+id/nav_list" android:id="@+id/nav_list"
android:icon="@drawable/ic_baseline_view_list_24" android:icon="@drawable/ic_baseline_view_list_24"
android:title="@string/action_lists" /> android:title="@string/action_lists" />
<item
android:id="@+id/nav_announcements"
android:icon="@drawable/ic_baseline_message_24"
android:title="@string/action_announcements"
android:visible="true" />
<item <item
android:id="@+id/nav_scheduled" android:id="@+id/nav_scheduled"
android:icon="@drawable/ic_baseline_schedule_24" android:icon="@drawable/ic_baseline_schedule_24"

View file

@ -1626,6 +1626,7 @@
<string name="msg_save_image">Are you want to exit without saving image ?</string> <string name="msg_save_image">Are you want to exit without saving image ?</string>
<string name="msg_share_image">Share Image</string> <string name="msg_share_image">Share Image</string>
<string name="tap_here_to_refresh_poll">Tap here to refresh poll</string> <string name="tap_here_to_refresh_poll">Tap here to refresh poll</string>
<string name="action_announcement_from_to">Announcement · %1$s - %2$s</string>
<string-array name="photo_editor_emoji" translatable="false"> <string-array name="photo_editor_emoji" translatable="false">
<!-- Smiles --> <!-- Smiles -->