Add support for creating Megaphones. Includes reactions megaphone.

fork-5.53.8
Greyson Parrelli 2020-01-22 09:22:19 -05:00
rodzic ef4c7e96da
commit 22f9bfeceb
29 zmienionych plików z 1195 dodań i 45 usunięć

Wyświetl plik

@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
@ -155,6 +155,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
}
@Override
@ -248,6 +249,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
TextSecurePreferences.setJobManagerVersion(this, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
}

Wyświetl plik

@ -36,8 +36,8 @@ public class BasicIntroFragment extends Fragment {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
drawable = getArguments().getInt(ARG_DRAWABLE);
text = getArguments().getInt(ARG_TEXT );
subtext = getArguments().getInt(ARG_SUBTEXT );
text = getArguments().getInt(ARG_TEXT);
subtext = getArguments().getInt(ARG_SUBTEXT);
}
}

Wyświetl plik

@ -29,7 +29,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@ -41,13 +40,16 @@ import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.snackbar.Snackbar;
import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes;
import androidx.annotation.WorkerThread;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.TooltipCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
@ -57,7 +59,7 @@ import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -77,7 +79,6 @@ import org.thoughtcrime.securesms.MainNavigator;
import org.thoughtcrime.securesms.NewConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
@ -93,9 +94,6 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -110,6 +108,10 @@ import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneListener;
import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
@ -137,7 +139,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
ActionMode.Callback,
ItemClickListener,
ConversationListSearchAdapter.EventListener,
MainNavigator.BackHandler
MainNavigator.BackHandler,
MegaphoneListener
{
private static final String TAG = Log.tag(ConversationListFragment.class);
@ -163,6 +166,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
private ConversationListAdapter defaultAdapter;
private ConversationListSearchAdapter searchAdapter;
private StickyHeaderDecoration searchAdapterDecoration;
private ViewGroup megaphoneContainer;
public static ConversationListFragment newInstance() {
return new ConversationListFragment();
@ -181,16 +185,17 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
reminderView = view.findViewById(R.id.reminder);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
emptyImage = view.findViewById(R.id.empty);
searchEmptyState = view.findViewById(R.id.search_no_results);
searchToolbar = view.findViewById(R.id.search_toolbar);
searchAction = view.findViewById(R.id.search_action);
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
reminderView = view.findViewById(R.id.reminder);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
emptyImage = view.findViewById(R.id.empty);
searchEmptyState = view.findViewById(R.id.search_no_results);
searchToolbar = view.findViewById(R.id.search_toolbar);
searchAction = view.findViewById(R.id.search_action);
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
megaphoneContainer = view.findViewById(R.id.megaphone_container);
Toolbar toolbar = view.findViewById(getToolbarRes());
toolbar.setVisibility(View.VISIBLE);
@ -339,6 +344,26 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
});
}
@Override
public void onMegaphoneNavigationRequested(@NonNull Intent intent) {
startActivity(intent);
}
@Override
public void onMegaphoneToastRequested(int stringRes) {
Toast.makeText(requireContext(), stringRes, Toast.LENGTH_SHORT).show();
}
@Override
public void onMegaphoneSnooze(@NonNull Megaphone megaphone) {
viewModel.onMegaphoneSnoozed(megaphone);
}
@Override
public void onMegaphoneCompleted(@NonNull Megaphone megaphone) {
viewModel.onMegaphoneCompleted(megaphone.getEvent());
}
private void initializeProfileIcon(@NonNull Recipient recipient) {
ImageView icon = requireView().findViewById(R.id.toolbar_icon);
@ -406,19 +431,52 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory()).get(ConversationListViewModel.class);
viewModel.getSearchResult().observe(this, result -> {
result = result != null ? result : SearchResult.EMPTY;
searchAdapter.updateResults(result);
viewModel.getSearchResult().observe(this, this::onSearchResultChanged);
viewModel.getMegaphone().observe(this, this::onMegaphoneChanged);
if (result.isEmpty() && activeAdapter == searchAdapter) {
searchEmptyState.setText(getString(R.string.SearchFragment_no_results, result.getQuery()));
searchEmptyState.setVisibility(View.VISIBLE);
} else {
searchEmptyState.setVisibility(View.GONE);
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
@Override
public void onStart(@NonNull LifecycleOwner owner) {
viewModel.onVisible();
}
});
}
private void onSearchResultChanged(@Nullable SearchResult result) {
result = result != null ? result : SearchResult.EMPTY;
searchAdapter.updateResults(result);
if (result.isEmpty() && activeAdapter == searchAdapter) {
searchEmptyState.setText(getString(R.string.SearchFragment_no_results, result.getQuery()));
searchEmptyState.setVisibility(View.VISIBLE);
} else {
searchEmptyState.setVisibility(View.GONE);
}
}
private void onMegaphoneChanged(@Nullable Megaphone megaphone) {
if (megaphone == null) {
megaphoneContainer.setVisibility(View.GONE);
megaphoneContainer.removeAllViews();
return;
}
View view = MegaphoneViewBuilder.build(requireContext(), megaphone, this);
megaphoneContainer.removeAllViews();
if (view != null) {
megaphoneContainer.addView(view);
megaphoneContainer.setVisibility(View.VISIBLE);
} else {
megaphoneContainer.setVisibility(View.GONE);
if (megaphone.getOnVisibleListener() != null) {
megaphone.getOnVisibleListener().onVisible(megaphone, this);
}
}
}
private void updateReminders() {
Context context = requireContext();

Wyświetl plik

@ -14,6 +14,10 @@ import android.text.TextUtils;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
@ -21,19 +25,23 @@ import org.thoughtcrime.securesms.util.Util;
class ConversationListViewModel extends ViewModel {
private final Application application;
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final ContentObserver observer;
private String lastQuery;
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository) {
this.application = application;
this.searchResult = new MutableLiveData<>();
this.searchRepository = searchRepository;
this.debouncer = new Debouncer(300);
this.observer = new ContentObserver(new Handler()) {
this.application = application;
this.megaphone = new MutableLiveData<>();
this.searchResult = new MutableLiveData<>();
this.searchRepository = searchRepository;
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
this.debouncer = new Debouncer(300);
this.observer = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
if (!TextUtils.isEmpty(getLastQuery())) {
@ -49,6 +57,24 @@ class ConversationListViewModel extends ViewModel {
return searchResult;
}
@NonNull LiveData<Megaphone> getMegaphone() {
return megaphone;
}
void onVisible() {
megaphoneRepository.getNextMegaphone(megaphone::postValue);
}
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
megaphone.postValue(null);
megaphoneRepository.markFinished(event);
}
void onMegaphoneSnoozed(@NonNull Megaphone snoozed) {
megaphoneRepository.markSeen(snoozed);
megaphone.postValue(null);
}
void updateQuery(String query) {
lastQuery = query;
debouncer.publish(() -> searchRepository.query(query, result -> {

Wyświetl plik

@ -61,6 +61,7 @@ public class DatabaseFactory {
private final StickerDatabase stickerDatabase;
private final StorageKeyDatabase storageKeyDatabase;
private final KeyValueDatabase keyValueDatabase;
private final MegaphoneDatabase megaphoneDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@ -155,6 +156,10 @@ public class DatabaseFactory {
return getInstance(context).keyValueDatabase;
}
public static MegaphoneDatabase getMegaphoneDatabase(Context context) {
return getInstance(context).megaphoneDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase();
}
@ -193,6 +198,7 @@ public class DatabaseFactory {
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

Wyświetl plik

@ -0,0 +1,97 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* IMPORTANT: Writes should only be made through {@link org.thoughtcrime.securesms.megaphone.MegaphoneRepository}.
*/
public class MegaphoneDatabase extends Database {
private static final String TABLE_NAME = "megaphone";
private static final String ID = "_id";
private static final String EVENT = "event";
private static final String SEEN_COUNT = "seen_count";
private static final String LAST_SEEN = "last_seen";
private static final String FINISHED = "finished";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
EVENT + " TEXT UNIQUE, " +
SEEN_COUNT + " INTEGER, " +
LAST_SEEN + " INTEGER, " +
FINISHED + " INTEGER)";
MegaphoneDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public void insert(@NonNull Collection<Event> events) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (Event event : events) {
ContentValues values = new ContentValues();
values.put(EVENT, event.getKey());
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public @NonNull List<MegaphoneRecord> getAll() {
List<MegaphoneRecord> records = new ArrayList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
String event = cursor.getString(cursor.getColumnIndexOrThrow(EVENT));
int seenCount = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_COUNT));
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SEEN));
boolean finished = cursor.getInt(cursor.getColumnIndexOrThrow(FINISHED)) == 1;
records.add(new MegaphoneRecord(Event.fromKey(event), seenCount, lastSeen, finished));
}
}
return records;
}
public void markSeen(@NonNull Event event, int seenCount, long lastSeen) {
String query = EVENT + " = ?";
String[] args = new String[]{event.getKey()};
ContentValues values = new ContentValues();
values.put(SEEN_COUNT, seenCount);
values.put(LAST_SEEN, lastSeen);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
}
public void markFinished(@NonNull Event event) {
String query = EVENT + " = ?";
String[] args = new String[]{event.getKey()};
ContentValues values = new ContentValues();
values.put(FINISHED, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
}
}

Wyświetl plik

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
@ -102,8 +103,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int ATTACHMENT_DISPLAY_ORDER = 42;
private static final int SPLIT_PROFILE_NAMES = 43;
private static final int STICKER_PACK_ORDER = 44;
private static final int MEGAPHONES = 45;
private static final int DATABASE_VERSION = 44;
private static final int DATABASE_VERSION = 45;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -146,6 +148,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(StickerDatabase.CREATE_TABLE);
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
db.execSQL(KeyValueDatabase.CREATE_TABLE);
db.execSQL(MegaphoneDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, JobDatabase.CREATE_TABLE);
@ -708,6 +711,14 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE sticker ADD COLUMN pack_order INTEGER DEFAULT 0");
}
if (oldVersion < MEGAPHONES) {
db.execSQL("CREATE TABLE megaphone (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"event TEXT UNIQUE, " +
"seen_count INTEGER, " +
"last_seen INTEGER, " +
"finished INTEGER)");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

Wyświetl plik

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.database.model;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.megaphone.Megaphones;
public class MegaphoneRecord {
private final Megaphones.Event event;
private final int seenCount;
private final long lastSeen;
private final boolean finished;
public MegaphoneRecord(@NonNull Megaphones.Event event, int seenCount, long lastSeen, boolean finished) {
this.event = event;
this.seenCount = seenCount;
this.lastSeen = lastSeen;
this.finished = finished;
}
public @NonNull Megaphones.Event getEvent() {
return event;
}
public int getSeenCount() {
return seenCount;
}
public long getLastSeen() {
return lastSeen;
}
public boolean isFinished() {
return finished;
}
}

Wyświetl plik

@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.IncomingMessageProcessor;
import org.thoughtcrime.securesms.gcm.MessageRetriever;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
@ -42,6 +43,7 @@ public class ApplicationDependencies {
private static JobManager jobManager;
private static FrameRateTracker frameRateTracker;
private static KeyValueStore keyValueStore;
private static MegaphoneRepository megaphoneRepository;
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) {
@ -167,6 +169,16 @@ public class ApplicationDependencies {
return keyValueStore;
}
public static synchronized @NonNull MegaphoneRepository getMegaphoneRepository() {
assertInitialization();
if (megaphoneRepository == null) {
megaphoneRepository = provider.provideMegaphoneRepository();
}
return megaphoneRepository;
}
private static void assertInitialization() {
if (application == null || provider == null) {
throw new UninitializedException();
@ -184,6 +196,7 @@ public class ApplicationDependencies {
@NonNull JobManager provideJobManager();
@NonNull FrameRateTracker provideFrameRateTracker();
@NonNull KeyValueStore provideKeyValueStore();
@NonNull MegaphoneRepository provideMegaphoneRepository();
}
private static class UninitializedException extends IllegalStateException {

Wyświetl plik

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
@ -125,6 +126,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
return new KeyValueStore(context);
}
@Override
public @NonNull MegaphoneRepository provideMegaphoneRepository() {
return new MegaphoneRepository(context);
}
private static class DynamicCredentialsProvider implements CredentialsProvider {
private final Context context;

Wyświetl plik

@ -0,0 +1,99 @@
package org.thoughtcrime.securesms.megaphone;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
public class BasicMegaphoneView extends FrameLayout {
private ImageView image;
private TextView titleText;
private TextView bodyText;
private Button actionButton;
private Button snoozeButton;
private Megaphone megaphone;
private MegaphoneListener megaphoneListener;
public BasicMegaphoneView(@NonNull Context context) {
super(context);
init(context);
}
public BasicMegaphoneView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(@NonNull Context context) {
inflate(context, R.layout.basic_megaphone_view, this);
this.image = findViewById(R.id.basic_megaphone_image);
this.titleText = findViewById(R.id.basic_megaphone_title);
this.bodyText = findViewById(R.id.basic_megaphone_body);
this.actionButton = findViewById(R.id.basic_megaphone_action);
this.snoozeButton = findViewById(R.id.basic_megaphone_snooze);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
megaphone.getOnVisibleListener().onVisible(megaphone, megaphoneListener);
}
}
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener megaphoneListener) {
this.megaphone = megaphone;
this.megaphoneListener = megaphoneListener;
if (megaphone.getImage() != 0) {
image.setVisibility(VISIBLE);
image.setImageResource(megaphone.getImage());
} else {
image.setVisibility(GONE);
}
if (megaphone.getTitle() != 0) {
titleText.setVisibility(VISIBLE);
titleText.setText(megaphone.getTitle());
} else {
titleText.setVisibility(GONE);
}
if (megaphone.getBody() != 0) {
bodyText.setVisibility(VISIBLE);
bodyText.setText(megaphone.getBody());
} else {
bodyText.setVisibility(GONE);
}
if (megaphone.getButtonText() != 0) {
actionButton.setVisibility(VISIBLE);
actionButton.setText(megaphone.getButtonText());
actionButton.setOnClickListener(v -> {
if (megaphone.getButtonClickListener() != null) {
megaphone.getButtonClickListener().onClick(megaphone, megaphoneListener);
}
});
} else {
actionButton.setVisibility(GONE);
}
if (megaphone.canSnooze()) {
snoozeButton.setVisibility(VISIBLE);
snoozeButton.setOnClickListener(v -> megaphoneListener.onMegaphoneSnooze(megaphone));
} else {
actionButton.setVisibility(GONE);
}
}
}

Wyświetl plik

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.megaphone;
final class ForeverSchedule implements MegaphoneSchedule {
private final boolean enabled;
ForeverSchedule(boolean enabled) {
this.enabled = enabled;
}
@Override
public boolean shouldDisplay(int seenCount, long lastSeen, long currentTime) {
return enabled;
}
}

Wyświetl plik

@ -0,0 +1,167 @@
package org.thoughtcrime.securesms.megaphone;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
/**
* For guidance on creating megaphones, see {@link Megaphones}.
*/
public class Megaphone {
/** For {@link #getMaxAppearances()}. */
public static final int UNLIMITED = -1;
private final Event event;
private final Style style;
private final boolean mandatory;
private final boolean canSnooze;
private final int maxAppearances;
private final int titleRes;
private final int bodyRes;
private final int imageRes;
private final int buttonTextRes;
private final OnClickListener buttonListener;
private final OnVisibleListener onVisibleListener;
private Megaphone(@NonNull Builder builder) {
this.event = builder.event;
this.style = builder.style;
this.mandatory = builder.mandatory;
this.canSnooze = builder.canSnooze;
this.maxAppearances = builder.maxAppearances;
this.titleRes = builder.titleRes;
this.bodyRes = builder.bodyRes;
this.imageRes = builder.imageRes;
this.buttonTextRes = builder.buttonTextRes;
this.buttonListener = builder.buttonListener;
this.onVisibleListener = builder.onVisibleListener;
}
public @NonNull Event getEvent() {
return event;
}
public boolean isMandatory() {
return mandatory;
}
public int getMaxAppearances() {
return maxAppearances;
}
public boolean canSnooze() {
return canSnooze;
}
public @NonNull Style getStyle() {
return style;
}
public @StringRes int getTitle() {
return titleRes;
}
public @StringRes int getBody() {
return bodyRes;
}
public @DrawableRes int getImage() {
return imageRes;
}
public @StringRes int getButtonText() {
return buttonTextRes;
}
public @Nullable OnClickListener getButtonClickListener() {
return buttonListener;
}
public @Nullable OnVisibleListener getOnVisibleListener() {
return onVisibleListener;
}
public static class Builder {
private final Event event;
private final Style style;
private boolean mandatory;
private boolean canSnooze;
private int maxAppearances;
private int titleRes;
private int bodyRes;
private int imageRes;
private int buttonTextRes;
private OnClickListener buttonListener;
private OnVisibleListener onVisibleListener;
public Builder(@NonNull Event event, @NonNull Style style) {
this.event = event;
this.style = style;
this.maxAppearances = 1;
}
public @NonNull Builder setMandatory(boolean mandatory) {
this.mandatory = mandatory;
return this;
}
public @NonNull Builder setSnooze(boolean canSnooze) {
this.canSnooze = canSnooze;
return this;
}
public @NonNull Builder setMaxAppearances(int maxAppearances) {
this.maxAppearances = maxAppearances;
return this;
}
public @NonNull Builder setTitle(@StringRes int titleRes) {
this.titleRes = titleRes;
return this;
}
public @NonNull Builder setBody(@StringRes int bodyRes) {
this.bodyRes = bodyRes;
return this;
}
public @NonNull Builder setImage(@DrawableRes int imageRes) {
this.imageRes = imageRes;
return this;
}
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull OnClickListener listener) {
this.buttonTextRes = buttonTextRes;
this.buttonListener = listener;
return this;
}
public @NonNull Builder setOnVisibleListener(@Nullable OnVisibleListener listener) {
this.onVisibleListener = listener;
return this;
}
public @NonNull Megaphone build() {
return new Megaphone(this);
}
}
enum Style {
REACTIONS, BASIC, FULLSCREEN
}
public interface OnVisibleListener {
void onVisible(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
}
public interface OnClickListener {
void onClick(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
}
}

Wyświetl plik

@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.megaphone;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
public interface MegaphoneListener {
/**
* When a megaphone wants to navigate to a specific intent.
*/
void onMegaphoneNavigationRequested(@NonNull Intent intent);
/**
* When a megaphone wants to show a toast/snackbar.
*/
void onMegaphoneToastRequested(@StringRes int stringRes);
/**
* When a megaphone has been snoozed via "remind me later" or a similar option.
*/
void onMegaphoneSnooze(@NonNull Megaphone megaphone);
/**
* Called when a megaphone completed its goal.
*/
void onMegaphoneCompleted(@NonNull Megaphone megaphone);
}

Wyświetl plik

@ -0,0 +1,126 @@
package org.thoughtcrime.securesms.megaphone;
import android.content.Context;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* Synchronization of data structures is done using a serial executor. Do not access or change
* data structures or fields on anything except the executor.
*/
public class MegaphoneRepository {
private final Context context;
private final Executor executor;
private final MegaphoneDatabase database;
private final Map<Event, MegaphoneRecord> databaseCache;
private boolean enabled;
public MegaphoneRepository(@NonNull Context context) {
this.context = context;
this.executor = SignalExecutors.SERIAL;
this.database = DatabaseFactory.getMegaphoneDatabase(context);
this.databaseCache = new HashMap<>();
executor.execute(this::init);
}
/**
* Marks any megaphones a new user shouldn't see as "finished".
*/
@MainThread
public void onFirstEverAppLaunch() {
executor.execute(() -> {
// Future megaphones we don't want to show to new users should get marked as finished here.
});
}
@MainThread
public void onAppForegrounded() {
executor.execute(() -> enabled = true);
}
@MainThread
public void getNextMegaphone(@NonNull Callback<Megaphone> callback) {
executor.execute(() -> {
if (enabled) {
callback.onResult(Megaphones.getNextMegaphone(context, databaseCache));
} else {
callback.onResult(null);
}
});
}
@MainThread
public void markSeen(@NonNull Megaphone megaphone) {
long lastSeen = System.currentTimeMillis();
executor.execute(() -> {
Event event = megaphone.getEvent();
MegaphoneRecord record = getRecord(event);
if (megaphone.getMaxAppearances() != Megaphone.UNLIMITED &&
record.getSeenCount() + 1 >= megaphone.getMaxAppearances())
{
database.markFinished(event);
} else {
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
}
enabled = false;
resetDatabaseCache();
});
}
@MainThread
public void markFinished(@NonNull Event event) {
executor.execute(() -> {
database.markFinished(event);
resetDatabaseCache();
});
}
@WorkerThread
private void init() {
List<MegaphoneRecord> records = database.getAll();
Set<Event> events = Stream.of(records).map(MegaphoneRecord::getEvent).collect(Collectors.toSet());
Set<Event> missing = Stream.of(Megaphones.Event.values()).filterNot(events::contains).collect(Collectors.toSet());
database.insert(missing);
resetDatabaseCache();
}
@WorkerThread
private @NonNull MegaphoneRecord getRecord(@NonNull Event event) {
//noinspection ConstantConditions
return databaseCache.get(event);
}
@WorkerThread
private void resetDatabaseCache() {
databaseCache.clear();
databaseCache.putAll(Stream.of(database.getAll()).collect(Collectors.toMap(MegaphoneRecord::getEvent, m -> m)));
}
public interface Callback<E> {
void onResult(E result);
}
}

Wyświetl plik

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.megaphone;
public interface MegaphoneSchedule {
boolean shouldDisplay(int seenCount, long lastSeen, long currentTime);
}

Wyświetl plik

@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.megaphone;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.reactions.ReactionsMegaphoneView;
public class MegaphoneViewBuilder {
public static @Nullable View build(@NonNull Context context,
@NonNull Megaphone megaphone,
@NonNull MegaphoneListener listener)
{
switch (megaphone.getStyle()) {
case BASIC:
return buildBasicMegaphone(context, megaphone, listener);
case FULLSCREEN:
return null;
case REACTIONS:
return buildReactionsMegaphone(context, megaphone, listener);
default:
throw new IllegalArgumentException("No view implemented for style!");
}
}
private static @NonNull View buildBasicMegaphone(@NonNull Context context,
@NonNull Megaphone megaphone,
@NonNull MegaphoneListener listener)
{
BasicMegaphoneView view = new BasicMegaphoneView(context);
view.present(megaphone, listener);
return view;
}
private static @NonNull View buildReactionsMegaphone(@NonNull Context context,
@NonNull Megaphone megaphone,
@NonNull MegaphoneListener listener)
{
ReactionsMegaphoneView view = new ReactionsMegaphoneView(context);
view.present(megaphone, listener);
return view;
}
}

Wyświetl plik

@ -0,0 +1,117 @@
package org.thoughtcrime.securesms.megaphone;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.FeatureFlags;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Creating a new megaphone:
* - Add an enum to {@link Event}
* - Return a megaphone in {@link #forRecord(MegaphoneRecord)}
* - Include the event in {@link #buildDisplayOrder()}
*
* Common patterns:
* - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}.
* - For events guarded by feature flags, set a {@link ForeverSchedule} with false in
* {@link #buildDisplayOrder()}.
* - For events that change, return different megaphones in {@link #forRecord(MegaphoneRecord)}
* based on whatever properties you're interested in.
*/
public final class Megaphones {
private Megaphones() {}
static @Nullable Megaphone getNextMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
long currentTime = System.currentTimeMillis();
List<Megaphone> megaphones = Stream.of(buildDisplayOrder())
.filter(e -> {
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
MegaphoneSchedule schedule = e.getValue();
return !record.isFinished() && schedule.shouldDisplay(record.getSeenCount(), record.getLastSeen(), currentTime);
})
.map(Map.Entry::getKey)
.map(records::get)
.map(Megaphones::forRecord)
.toList();
boolean hasOptional = Stream.of(megaphones).anyMatch(m -> !m.isMandatory());
boolean hasMandatory = Stream.of(megaphones).anyMatch(Megaphone::isMandatory);
if (hasOptional && hasMandatory) {
megaphones = Stream.of(megaphones).filter(Megaphone::isMandatory).toList();
}
if (megaphones.size() > 0) {
return megaphones.get(0);
} else {
return null;
}
}
/**
* This is when you would hide certain megaphones based on {@link FeatureFlags}. You could
* conditionally set a {@link ForeverSchedule} set to false for disabled features.
*/
private static Map<Event, MegaphoneSchedule> buildDisplayOrder() {
return new LinkedHashMap<Event, MegaphoneSchedule>() {{
put(Event.REACTIONS, new ForeverSchedule(FeatureFlags.reactionSending()));
}};
}
private static @NonNull Megaphone forRecord(@NonNull MegaphoneRecord record) {
switch (record.getEvent()) {
case REACTIONS:
return buildReactionsMegaphone();
default:
throw new IllegalArgumentException("Event not handled!");
}
}
private static @NonNull Megaphone buildReactionsMegaphone() {
return new Megaphone.Builder(Event.REACTIONS, Megaphone.Style.REACTIONS)
.setMaxAppearances(Megaphone.UNLIMITED)
.setMandatory(false)
.build();
}
public enum Event {
REACTIONS("reactions");
private final String key;
Event(@NonNull String key) {
this.key = key;
}
public @NonNull String getKey() {
return key;
}
public static Event fromKey(@NonNull String key) {
for (Event event : values()) {
if (event.getKey().equals(key)) {
return event;
}
}
throw new IllegalArgumentException("No event for key: " + key);
}
}
}

Wyświetl plik

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.megaphone;
class RecurringSchedule implements MegaphoneSchedule {
private final long[] gaps;
RecurringSchedule(long... durationGaps) {
this.gaps = durationGaps;
}
@Override
public boolean shouldDisplay(int seenCount, long lastSeen, long currentTime) {
if (seenCount == 0) {
return true;
}
long gap = gaps[Math.min(seenCount - 1, gaps.length - 1)];
return lastSeen + gap <= currentTime ;
}
}

Wyświetl plik

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.reactions;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneListener;
public class ReactionsMegaphoneView extends FrameLayout {
private View closeButton;
public ReactionsMegaphoneView(Context context) {
super(context);
initialize(context);
}
public ReactionsMegaphoneView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
private void initialize(@NonNull Context context) {
inflate(context, R.layout.reactions_megaphone, this);
this.closeButton = findViewById(R.id.reactions_megaphone_x);
}
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener) {
this.closeButton.setOnClickListener(v -> listener.onMegaphoneCompleted(megaphone));
}
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 11 KiB

Wyświetl plik

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="125dp"
android:viewportWidth="335"
android:viewportHeight="210">
<path
android:pathData="M118.928,28.8C118.928,18.719 118.928,13.679 120.89,9.828C122.616,6.441 125.37,3.688 128.757,1.962C132.607,0 137.647,0 147.728,0H306.2C316.281,0 321.321,0 325.172,1.962C328.559,3.688 331.312,6.441 333.038,9.828C335,13.679 335,18.719 335,28.8V74.627C335,76.867 335,77.987 334.564,78.843C334.181,79.595 333.569,80.207 332.816,80.591C331.96,81.027 330.84,81.027 328.6,81.027H147.728C137.647,81.027 132.607,81.027 128.757,79.065C125.37,77.339 122.616,74.586 120.89,71.199C118.928,67.348 118.928,62.308 118.928,52.227V28.8ZM226.964,103.534C226.964,101.439 226.964,100.391 227.051,99.508C227.895,90.966 234.653,84.209 243.195,83.365C244.077,83.278 245.125,83.278 247.221,83.278H328.6C330.84,83.278 331.96,83.278 332.816,83.714C333.569,84.097 334.181,84.709 334.564,85.462C335,86.317 335,87.437 335,89.678V94.991C335,105.072 335,110.112 333.038,113.963C331.312,117.35 328.559,120.103 325.172,121.829C321.321,123.791 316.281,123.791 306.2,123.791H247.221C245.125,123.791 244.077,123.791 243.195,123.704C234.653,122.86 227.895,116.102 227.051,107.561C226.964,106.678 226.964,105.63 226.964,103.534ZM1.962,138.121C0,141.971 0,147.012 0,157.093V180.519C0,190.6 0,195.641 1.962,199.491C3.688,202.878 6.441,205.632 9.828,207.358C13.679,209.319 18.719,209.319 28.8,209.319H187.272C197.353,209.319 202.393,209.319 206.243,207.358C209.63,205.632 212.384,202.878 214.11,199.491C216.072,195.641 216.072,190.6 216.072,180.519V157.093C216.072,147.012 216.072,141.971 214.11,138.121C212.384,134.734 209.63,131.98 206.243,130.254C202.393,128.293 197.353,128.293 187.272,128.293H28.8C18.719,128.293 13.679,128.293 9.828,130.254C6.441,131.98 3.688,134.734 1.962,138.121Z"
android:fillColor="#848484"
android:fillType="evenOdd"/>
</vector>

Wyświetl plik

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:background="?megaphone_background">
<ImageView
android:id="@+id/basic_megaphone_image"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:scaleType="centerInside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/profile_splash"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/basic_megaphone_title"
style="@style/Signal.Text.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif-medium"
app:layout_constraintStart_toEndOf="@id/basic_megaphone_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/basic_megaphone_image"
tools:text="Avengers HQ Destroyed!" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/basic_megaphone_body"
style="@style/Signal.Text.Preview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textColor="?megaphone_body_text_color"
app:layout_constraintStart_toStartOf="@id/basic_megaphone_title"
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_title"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Where was the 'hero' Spider-Man during the battle?" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/basic_megaphone_content_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="basic_megaphone_image,basic_megaphone_body,basic_megaphone_title"/>
<Space
android:id="@+id/basic_megaphone_spacer"
android:layout_width="0dp"
android:layout_height="8dp"
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/basic_megaphone_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Button.Borderless"
app:layout_constraintStart_toEndOf="@id/basic_megaphone_snooze"
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_spacer"
app:layout_constraintEnd_toEndOf="parent"
tools:text="*sigh*"
tools:visibility="visible"/>
<Button
android:id="@+id/basic_megaphone_snooze"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/Megaphones_remind_me_later"
style="@style/Button.Borderless"
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_spacer"
app:layout_constraintEnd_toStartOf="@id/basic_megaphone_action"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

Wyświetl plik

@ -76,8 +76,8 @@
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarStyle"
android:visibility="gone"
app:titleTextAppearance="@style/TextSecure.TitleTextStyle"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:titleTextAppearance="@style/TextSecure.TitleTextStyle" />
<org.thoughtcrime.securesms.components.SearchToolbar
android:id="@+id/search_toolbar"
@ -110,13 +110,13 @@
android:id="@+id/search_no_results"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:background="?attr/search_background"
android:gravity="center"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
tools:text="@string/SearchFragment_no_results" />
<LinearLayout
@ -156,7 +156,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
tools:visibility="gone"/>
tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
@ -170,7 +170,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/reminder"
tools:listitem="@layout/conversation_list_item_view"
tools:visibility="gone"/>
tools:visibility="gone" />
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/camera_fab"
@ -193,8 +193,27 @@
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
android:focusable="true"
android:tint="?conversation_list_compose_icon_tint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/megaphone_container"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_compose_solid_24" />
<androidx.cardview.widget.CardView
android:id="@+id/megaphone_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clipToPadding="false"
android:clipChildren="false"
android:visibility="gone"
app:contentPadding="0dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:cardBackgroundColor="?megaphone_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:clipChildren="false"
android:clipToPadding="false"
tools:parentTag="android.widget.FrameLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="14dp">
<ImageView
android:id="@+id/reactions_megaphone_banner_background"
android:layout_width="0dp"
android:layout_height="125dp"
android:background="?megaphone_reactions_shade"
android:scaleType="centerCrop"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:alpha="0.5"
app:srcCompat="@drawable/reactions_megaphone_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/reactions_megaphone_animation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_rawRes="@raw/lottie_reactions_megaphone"
app:lottie_autoPlay="true"
app:lottie_repeatCount="3"
app:layout_constraintStart_toStartOf="@id/reactions_megaphone_banner_background"
app:layout_constraintEnd_toEndOf="@id/reactions_megaphone_banner_background"
app:layout_constraintTop_toTopOf="@id/reactions_megaphone_banner_background"
app:layout_constraintBottom_toBottomOf="@id/reactions_megaphone_banner_background"
tools:layout_width="200dp"
tools:layout_height="50dp"/>
<ImageView
android:id="@+id/reactions_megaphone_x"
android:layout_width="46dp"
android:layout_height="46dp"
android:padding="13dp"
android:tint="?megaphone_reactions_close_tint"
android:background="?selectableItemBackgroundBorderless"
app:srcCompat="@drawable/ic_x"
app:layout_constraintEnd_toEndOf="@id/reactions_megaphone_banner_background"
app:layout_constraintTop_toTopOf="@id/reactions_megaphone_banner_background"/>
<TextView
android:id="@+id/reactions_megaphone_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="14dp"
style="@style/Signal.Text.Body"
android:fontFamily="sans-serif-medium"
android:text="@string/Megaphones_introducing_reactions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/reactions_megaphone_banner_background" />
<TextView
android:id="@+id/reactions_megaphone_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColor="?megaphone_body_text_color"
style="@style/Signal.Text.Preview"
android:text="@string/Megaphones_tap_and_hold_any_message_to_quicky_share_how_you_feel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/reactions_megaphone_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -247,6 +247,12 @@
<attr name="login_top_background" format="color"/>
<attr name="login_floating_background" format="reference"/>
<attr name="megaphone_background" format="color"/>
<attr name="megaphone_background_shadow" format="color|reference"/>
<attr name="megaphone_body_text_color" format="color"/>
<attr name="megaphone_reactions_shade" format="color"/>
<attr name="megaphone_reactions_close_tint" format="color"/>
<declare-styleable name="ColorPreference">
<attr name="itemLayout" format="reference" />
<attr name="choices" format="reference" />

Wyświetl plik

@ -531,7 +531,12 @@
<string name="MediaOverviewActivity_sent_by_s_to_s">Sent by %1$s to %2$s</string>
<string name="MediaOverviewActivity_sent_by_you_to_s">Sent by you to %1$s</string>
<!--- NotificationBarManager -->
<!-- Megaphones -->
<string name="Megaphones_introducing_reactions">Introducing Reactions</string>
<string name="Megaphones_tap_and_hold_any_message_to_quicky_share_how_you_feel">Tap and hold any message to quickly share how you feel.</string>
<string name="Megaphones_remind_me_later">Remind me later</string>
<!-- NotificationBarManager -->
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
<string name="NotificationBarManager__establishing_signal_call">Establishing Signal call</string>
<string name="NotificationBarManager__incoming_signal_call">Incoming Signal call</string>

Wyświetl plik

@ -313,6 +313,12 @@
<item name="media_keyboard_button_color">@color/core_grey_60</item>
<item name="megaphone_background">@color/core_white</item>
<item name="megaphone_background_shadow">@drawable/megaphone_background_shadow</item>
<item name="megaphone_body_text_color">@color/core_grey_65</item>
<item name="megaphone_reactions_shade">@color/core_grey_02</item>
<item name="megaphone_reactions_close_tint">@color/core_black</item>
<item name="menu_new_conversation_icon">@drawable/ic_add_white_24dp</item>
<item name="menu_group_icon">@drawable/ic_group_solid_24</item>
<item name="menu_search_icon">@drawable/ic_search_24</item>
@ -556,6 +562,12 @@
<item name="media_keyboard_button_color">@color/core_grey_25</item>
<item name="megaphone_background">@color/core_grey_75</item>
<item name="megaphone_background_shadow">@null</item>
<item name="megaphone_body_text_color">@color/core_grey_25</item>
<item name="megaphone_reactions_shade">@color/core_grey_70</item>
<item name="megaphone_reactions_close_tint">@color/core_grey_15</item>
<item name="menu_new_conversation_icon">@drawable/ic_add_white_24dp</item>
<item name="menu_group_icon">@drawable/ic_group_solid_24</item>
<item name="menu_search_icon">@drawable/ic_search_24</item>