diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f6b36d7..f907ab4b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -150,6 +150,9 @@ + emojis; public static Account.API api; public static boolean admin; public static WeakReference accountWeakReference; @@ -351,6 +355,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } else if (id == R.id.nav_partnership) { Intent intent = new Intent(this, PartnerShipActivity.class); startActivity(intent); + } else if (id == R.id.nav_announcements) { + Intent intent = new Intent(this, AnnouncementActivity.class); + startActivity(intent); } binding.drawerLayout.close(); return false; @@ -724,6 +731,15 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE)); //For receiving data from other activities 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; }); popup.show(); + } public void refreshFragment() { diff --git a/app/src/main/java/app/fedilab/android/activities/AnnouncementActivity.java b/app/src/main/java/app/fedilab/android/activities/AnnouncementActivity.java new file mode 100644 index 00000000..2cfd5a1b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/AnnouncementActivity.java @@ -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 . */ + + +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); + } + +} diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAnnouncementsService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAnnouncementsService.java index ac3dea56..1ce3a2e6 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAnnouncementsService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAnnouncementsService.java @@ -30,28 +30,27 @@ import retrofit2.http.Query; public interface MastodonAnnouncementsService { - @GET("/announcements") + @GET("announcements") Call> getAnnouncements( @Header("Authorization") String token, - @Query("with_dismissed") boolean with_dismissed + @Query("with_dismissed") Boolean with_dismissed ); @FormUrlEncoded - @POST("/announcements/{id}/dismiss") + @POST("announcements/{id}/dismiss") Call dismiss( @Header("Authorization") String app_token, @Path("id") String id ); - @FormUrlEncoded - @PUT("/announcements/{id}/reactions/{name}") + @PUT("announcements/{id}/reactions/{name}") Call addReaction( @Header("Authorization") String app_token, @Path("id") String id, @Path("name") String name ); - @DELETE("/announcements/{id}/reactions/{name}") + @DELETE("announcements/{id}/reactions/{name}") Call removeReaction( @Header("Authorization") String app_token, @Path("id") String id, diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java b/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java index e73ec120..4669833d 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java @@ -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, * see . */ +import android.text.Spannable; + import com.google.gson.annotations.SerializedName; import java.util.Date; @@ -46,4 +48,7 @@ public class Announcement { public List emojis; @SerializedName("reactions") public List reactions; + + //Some extra spannable element - They will be filled automatically when fetching the status + public transient Spannable span_content; } diff --git a/app/src/main/java/app/fedilab/android/helper/Helper.java b/app/src/main/java/app/fedilab/android/helper/Helper.java index 1d92ef6e..27e25d2e 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -602,7 +602,6 @@ public class Helper { public static void removeAccount(Activity activity, Account account) throws DBException { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); //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 instance = sharedpreferences.getString(PREF_USER_INSTANCE, null); Account accountDB = new Account(activity); @@ -881,6 +880,34 @@ public class Helper { 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 * diff --git a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java index f5204a67..1dd94a4e 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -76,6 +76,7 @@ import app.fedilab.android.activities.HashTagActivity; import app.fedilab.android.activities.MainActivity; import app.fedilab.android.activities.ProfileActivity; 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.Emoji; 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 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("= 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 emojiList = announcement.emojis; + //Will convert emoji if asked + if (emojiList != null && emojiList.size() > 0) { + for (Emoji emoji : emojiList) { + if (Helper.isValidContextForGlide(context)) { + FutureTarget 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> map = httpsURLConnection.getHeaderFields(); + for (Map.Entry> 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 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 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

tags in toots * @@ -586,6 +1007,24 @@ public class SpannableHelper { return statuses; } + + public static List convertAnnouncement(Context context, List 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) { if (status != null) { status.span_content = SpannableHelper.convert(context, status, status.content); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java new file mode 100644 index 00000000..0153460c --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AnnouncementAdapter.java @@ -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 . */ + + +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 { + + private final List announcements; + private Context context; + private AnnouncementsVM announcementsVM; + private AlertDialog alertDialogEmoji; + + public AnnouncementAdapter(List 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; + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java index 34390d98..c1b93872 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java @@ -15,6 +15,7 @@ package app.fedilab.android.ui.drawer; * see . */ +import static app.fedilab.android.BaseMainActivity.emojis; import static app.fedilab.android.BaseMainActivity.instanceInfo; import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE; @@ -124,7 +125,6 @@ public class ComposeAdapter extends RecyclerView.Adapter emojis; private int statusCount; private Context context; private AlertDialog alertDialogEmoji; @@ -1226,12 +1226,7 @@ public class ComposeAdapter extends RecyclerView.Adapter. */ + + +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 { + + private final List reactions; + private final String announcementId; + private Context context; + + ReactionAdapter(String announcementId, List 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; + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java new file mode 100644 index 00000000..695a9d4d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java @@ -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 . */ + +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 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; + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java index da36a04b..4421f04d 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -58,6 +58,7 @@ import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.drawer.StatusAdapter; 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.TimelinesVM; @@ -70,6 +71,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. private FragmentPaginationBinding binding; private TimelinesVM timelinesVM; private AccountsVM accountsVM; + private AnnouncementsVM announcementsVM; private boolean flagLoading; private List statuses; private String search, searchCache; @@ -237,6 +239,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.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.recyclerView.setVisibility(View.GONE); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java index c9ce5251..5b916e26 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import app.fedilab.android.client.endpoints.MastodonAnnouncementsService; import app.fedilab.android.client.entities.api.Announcement; import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.SpannableHelper; import okhttp3.OkHttpClient; import retrofit2.Call; 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. * @return {@link LiveData} containing a {@link List} of {@link Announcement}s */ - public LiveData> getAnnouncements(@NonNull String instance, String token, boolean withDismissed) { + public LiveData> getAnnouncements(@NonNull String instance, String token, Boolean withDismissed) { MastodonAnnouncementsService mastodonAnnouncementsService = init(instance); announcementListMutableLiveData = new MutableLiveData<>(); new Thread(() -> { @@ -78,6 +79,7 @@ public class AnnouncementsVM extends AndroidViewModel { Response> getAnnouncementsResponse = getAnnouncementsCall.execute(); if (getAnnouncementsResponse.isSuccessful()) { announcementList = getAnnouncementsResponse.body(); + SpannableHelper.convertAnnouncement(getApplication(), announcementList); } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/res/drawable/ic_baseline_add_reaction_24.xml b/app/src/main/res/drawable/ic_baseline_add_reaction_24.xml new file mode 100644 index 00000000..5872363d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_add_reaction_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml b/app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml new file mode 100644 index 00000000..9300ca33 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_message_24.xml b/app/src/main/res/drawable/ic_baseline_message_24.xml index c9a3e3da..762268ce 100644 --- a/app/src/main/res/drawable/ic_baseline_message_24.xml +++ b/app/src/main/res/drawable/ic_baseline_message_24.xml @@ -2,7 +2,7 @@ android:width="24dp" android:height="24dp" android:autoMirrored="true" - android:tint="?attr/colorControlNormal" + android:tint="#FFFFFF" android:viewportWidth="24" android:viewportHeight="24"> + + + + diff --git a/app/src/main/res/drawable/reaction_voted.xml b/app/src/main/res/drawable/reaction_voted.xml new file mode 100644 index 00000000..267b5815 --- /dev/null +++ b/app/src/main/res/drawable/reaction_voted.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_announcement.xml b/app/src/main/res/layout/activity_announcement.xml new file mode 100644 index 00000000..92dbf9d7 --- /dev/null +++ b/app/src/main/res/layout/activity_announcement.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_announcement.xml b/app/src/main/res/layout/drawer_announcement.xml new file mode 100644 index 00000000..7f82092b --- /dev/null +++ b/app/src/main/res/layout/drawer_announcement.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_reaction.xml b/app/src/main/res/layout/drawer_reaction.xml new file mode 100644 index 00000000..84ada755 --- /dev/null +++ b/app/src/main/res/layout/drawer_reaction.xml @@ -0,0 +1,52 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index 87296e93..b7e788b4 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -28,6 +28,13 @@ android:id="@+id/nav_list" android:icon="@drawable/ic_baseline_view_list_24" android:title="@string/action_lists" /> + + + Are you want to exit without saving image ? Share Image Tap here to refresh poll + Announcement · %1$s - %2$s