kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add support for creating Megaphones. Includes reactions megaphone.
rodzic
ef4c7e96da
commit
22f9bfeceb
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
public interface MegaphoneSchedule {
|
||||
boolean shouldDisplay(int seenCount, long lastSeen, long currentTime);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue