kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add Notification profiles.
rodzic
31e0696395
commit
6c608e955e
|
@ -43,11 +43,13 @@
|
||||||
</JavaCodeStyleSettings>
|
</JavaCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
<value />
|
<value>
|
||||||
|
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||||
|
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||||
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="JAVA">
|
<codeStyleSettings language="JAVA">
|
||||||
<option name="BRACE_STYLE" value="5" />
|
<option name="BRACE_STYLE" value="5" />
|
||||||
|
@ -213,13 +215,5 @@
|
||||||
</rules>
|
</rules>
|
||||||
</arrangement>
|
</arrangement>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
<indentOptions>
|
|
||||||
<option name="INDENT_SIZE" value="2" />
|
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
|
||||||
<option name="TAB_SIZE" value="2" />
|
|
||||||
</indentOptions>
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
</component>
|
</component>
|
|
@ -549,6 +549,7 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.rxjava3.rxandroid
|
implementation libs.rxjava3.rxandroid
|
||||||
implementation libs.rxjava3.rxkotlin
|
implementation libs.rxjava3.rxkotlin
|
||||||
|
implementation libs.rxdogtag
|
||||||
|
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.database
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotEquals
|
import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
|
@ -14,12 +16,14 @@ import org.junit.runner.RunWith
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey
|
import org.signal.zkgroup.groups.GroupMasterKey
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.database.model.Mention
|
import org.thoughtcrime.securesms.database.model.Mention
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||||
|
@ -45,6 +49,7 @@ class RecipientDatabaseTest_merges {
|
||||||
private lateinit var sessionDatabase: SessionDatabase
|
private lateinit var sessionDatabase: SessionDatabase
|
||||||
private lateinit var mentionDatabase: MentionDatabase
|
private lateinit var mentionDatabase: MentionDatabase
|
||||||
private lateinit var reactionDatabase: ReactionDatabase
|
private lateinit var reactionDatabase: ReactionDatabase
|
||||||
|
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
@ -58,6 +63,7 @@ class RecipientDatabaseTest_merges {
|
||||||
sessionDatabase = SignalDatabase.sessions
|
sessionDatabase = SignalDatabase.sessions
|
||||||
mentionDatabase = SignalDatabase.mentions
|
mentionDatabase = SignalDatabase.mentions
|
||||||
reactionDatabase = SignalDatabase.reactions
|
reactionDatabase = SignalDatabase.reactions
|
||||||
|
notificationProfileDatabase = SignalDatabase.notificationProfiles
|
||||||
|
|
||||||
ensureDbEmpty()
|
ensureDbEmpty()
|
||||||
}
|
}
|
||||||
|
@ -68,6 +74,7 @@ class RecipientDatabaseTest_merges {
|
||||||
// Setup
|
// Setup
|
||||||
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||||
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||||
|
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_B)
|
||||||
|
|
||||||
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
|
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
|
||||||
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
||||||
|
@ -97,6 +104,14 @@ class RecipientDatabaseTest_merges {
|
||||||
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
|
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
|
||||||
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
|
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
|
||||||
|
|
||||||
|
val profile1: NotificationProfile = notificationProfile(name = "Test")
|
||||||
|
val profile2: NotificationProfile = notificationProfile(name = "Test2")
|
||||||
|
|
||||||
|
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
|
||||||
|
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
|
||||||
|
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
|
||||||
|
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
|
||||||
|
|
||||||
// Merge
|
// Merge
|
||||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
|
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
|
||||||
|
@ -171,6 +186,13 @@ class RecipientDatabaseTest_merges {
|
||||||
|
|
||||||
assertEquals(1, reactionsMms.size)
|
assertEquals(1, reactionsMms.size)
|
||||||
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
|
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
|
||||||
|
|
||||||
|
// Notification Profile validation
|
||||||
|
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
|
||||||
|
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
|
||||||
|
|
||||||
|
assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
|
||||||
|
assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val context: Application
|
private val context: Application
|
||||||
|
@ -220,6 +242,10 @@ class RecipientDatabaseTest_merges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notificationProfile(name: String): NotificationProfile {
|
||||||
|
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
|
||||||
|
}
|
||||||
|
|
||||||
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
|
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
|
||||||
data class MentionModel(
|
data class MentionModel(
|
||||||
val recipientId: RecipientId,
|
val recipientId: RecipientId,
|
||||||
|
|
|
@ -88,11 +88,17 @@ import org.thoughtcrime.securesms.util.VersionTracker;
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
import rxdogtag2.RxDogTag;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will be called once when the TextSecure process is created.
|
* Will be called once when the TextSecure process is created.
|
||||||
|
@ -138,10 +144,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
})
|
})
|
||||||
.addBlocking("crash-handling", this::initializeCrashHandling)
|
.addBlocking("crash-handling", this::initializeCrashHandling)
|
||||||
.addBlocking("rx-init", () -> {
|
.addBlocking("rx-init", this::initializeRx)
|
||||||
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
|
||||||
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
|
||||||
})
|
|
||||||
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
||||||
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
||||||
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
|
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
|
||||||
|
@ -280,6 +283,30 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler));
|
Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeRx() {
|
||||||
|
RxDogTag.install();
|
||||||
|
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
||||||
|
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
||||||
|
RxJavaPlugins.setErrorHandler(e -> {
|
||||||
|
boolean wasWrapped = false;
|
||||||
|
while ((e instanceof UndeliverableException || e instanceof AssertionError || e instanceof OnErrorNotImplementedException) && e.getCause() != null) {
|
||||||
|
wasWrapped = true;
|
||||||
|
e = e.getCause();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasWrapped && (e instanceof SocketException || e instanceof SocketTimeoutException || e instanceof InterruptedException)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.currentThread().getUncaughtExceptionHandler();
|
||||||
|
if (uncaughtExceptionHandler == null) {
|
||||||
|
uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeApplicationMigrations() {
|
private void initializeApplicationMigrations() {
|
||||||
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
|
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.EdgeEffect
|
import android.widget.EdgeEffect
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
@ -24,9 +25,10 @@ abstract class DSLSettingsFragment(
|
||||||
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||||
) : Fragment(layoutId) {
|
) : Fragment(layoutId) {
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
private var recyclerView: RecyclerView? = null
|
||||||
private lateinit var scrollAnimationHelper: OnScrollAnimationHelper
|
private var scrollAnimationHelper: OnScrollAnimationHelper? = null
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||||
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
||||||
|
@ -44,16 +46,23 @@ abstract class DSLSettingsFragment(
|
||||||
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView = view.findViewById(R.id.recycler)
|
|
||||||
recyclerView.edgeEffectFactory = EdgeEffectFactory()
|
|
||||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||||
val adapter = DSLSettingsAdapter()
|
val settingsAdapter = DSLSettingsAdapter()
|
||||||
|
|
||||||
recyclerView.layoutManager = layoutManagerProducer(requireContext())
|
recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply {
|
||||||
recyclerView.adapter = adapter
|
edgeEffectFactory = EdgeEffectFactory()
|
||||||
recyclerView.addOnScrollListener(scrollAnimationHelper)
|
layoutManager = layoutManagerProducer(requireContext())
|
||||||
|
adapter = settingsAdapter
|
||||||
|
addOnScrollListener(scrollAnimationHelper!!)
|
||||||
|
}
|
||||||
|
|
||||||
bindAdapter(adapter)
|
bindAdapter(settingsAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
recyclerView = null
|
||||||
|
scrollAnimationHelper = null
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
protected open fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||||
|
|
|
@ -53,6 +53,9 @@ sealed class DSLSettingsText {
|
||||||
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
return SpanUtil.color(textColor, charSequence)
|
return SpanUtil.color(textColor, charSequence)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean = textColor == (other as? ColorModifier)?.textColor
|
||||||
|
override fun hashCode(): Int = textColor
|
||||||
}
|
}
|
||||||
|
|
||||||
object CenterModifier : Modifier {
|
object CenterModifier : Modifier {
|
||||||
|
@ -68,6 +71,9 @@ sealed class DSLSettingsText {
|
||||||
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
return SpanUtil.textAppearance(context, textAppearance, charSequence)
|
return SpanUtil.textAppearance(context, textAppearance, charSequence)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean = textAppearance == (other as? TextAppearanceModifier)?.textAppearance
|
||||||
|
override fun hashCode(): Int = textAppearance
|
||||||
}
|
}
|
||||||
|
|
||||||
object BoldModifier : Modifier {
|
object BoldModifier : Modifier {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import io.reactivex.rxjava3.subjects.Subject
|
||||||
import org.thoughtcrime.securesms.MainActivity
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||||
import org.thoughtcrime.securesms.help.HelpFragment
|
import org.thoughtcrime.securesms.help.HelpFragment
|
||||||
|
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.util.CachedInflater
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||||
|
|
||||||
private const val START_LOCATION = "app.settings.start.location"
|
private const val START_LOCATION = "app.settings.start.location"
|
||||||
|
private const val START_ARGUMENTS = "app.settings.start.arguments"
|
||||||
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
||||||
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
||||||
|
|
||||||
|
@ -50,6 +52,11 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
|
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
|
||||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment()
|
StartLocation.BOOST -> AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment()
|
||||||
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
||||||
|
StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles()
|
||||||
|
StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles()
|
||||||
|
StartLocation.NOTIFICATION_PROFILE_DETAILS -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails(
|
||||||
|
EditNotificationProfileScheduleFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).profileId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +138,22 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun manageSubscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.MANAGE_SUBSCRIPTIONS)
|
fun manageSubscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.MANAGE_SUBSCRIPTIONS)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun notificationProfiles(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATION_PROFILES)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createNotificationProfile(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CREATE_NOTIFICATION_PROFILE)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun notificationProfileDetails(context: Context, profileId: Long): Intent {
|
||||||
|
val arguments = EditNotificationProfileScheduleFragmentArgs.Builder(profileId, false)
|
||||||
|
.build()
|
||||||
|
.toBundle()
|
||||||
|
|
||||||
|
return getIntentForStartLocation(context, StartLocation.NOTIFICATION_PROFILE_DETAILS)
|
||||||
|
.putExtra(START_ARGUMENTS, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||||
return Intent(context, AppSettingsActivity::class.java)
|
return Intent(context, AppSettingsActivity::class.java)
|
||||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||||
|
@ -147,7 +170,10 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||||
CHANGE_NUMBER(5),
|
CHANGE_NUMBER(5),
|
||||||
SUBSCRIPTIONS(6),
|
SUBSCRIPTIONS(6),
|
||||||
BOOST(7),
|
BOOST(7),
|
||||||
MANAGE_SUBSCRIPTIONS(8);
|
MANAGE_SUBSCRIPTIONS(8),
|
||||||
|
NOTIFICATION_PROFILES(9),
|
||||||
|
CREATE_NOTIFICATION_PROFILE(10),
|
||||||
|
NOTIFICATION_PROFILE_DETAILS(11);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCode(code: Int?): StartLocation {
|
fun fromCode(code: Int?): StartLocation {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
@ -218,6 +219,18 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||||
|
|
||||||
dividerPref()
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.NotificationsSettingsFragment__notification_profiles)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__profiles),
|
||||||
|
summary = DSLSettingsText.from(R.string.NotificationsSettingsFragment__set_up_notification_profiles),
|
||||||
|
onClick = {
|
||||||
|
findNavController().navigate(R.id.action_notificationsSettingsFragment_to_notificationProfilesFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
sectionHeaderPref(R.string.NotificationsSettingsFragment__notify_when)
|
sectionHeaderPref(R.string.NotificationsSettingsFragment__notify_when)
|
||||||
|
|
||||||
switchPref(
|
switchPref(
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.manual
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.models.NotificationProfileSelection
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BottomSheetDialogFragment that allows a user to select a notification profile to manually enable/disable.
|
||||||
|
*/
|
||||||
|
class NotificationProfileSelectionFragment : DSLSettingsBottomSheetFragment() {
|
||||||
|
|
||||||
|
private val viewModel: NotificationProfileSelectionViewModel by viewModels(
|
||||||
|
factoryProducer = {
|
||||||
|
NotificationProfileSelectionViewModel.Factory(NotificationProfilesRepository())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
NotificationProfileSelection.register(adapter)
|
||||||
|
|
||||||
|
recyclerView.itemAnimator = null
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
|
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: NotificationProfileSelectionState): DSLConfiguration {
|
||||||
|
val activeProfile: NotificationProfile? = NotificationProfiles.getActiveProfile(state.notificationProfiles)
|
||||||
|
|
||||||
|
return configure {
|
||||||
|
|
||||||
|
state.notificationProfiles.forEach { profile ->
|
||||||
|
customPref(
|
||||||
|
NotificationProfileSelection.Entry(
|
||||||
|
isOn = profile == activeProfile,
|
||||||
|
summary = if (profile == activeProfile) DSLSettingsText.from(NotificationProfiles.getActiveProfileDescription(requireContext(), profile)) else DSLSettingsText.from(R.string.NotificationProfileDetails__off),
|
||||||
|
notificationProfile = profile,
|
||||||
|
isExpanded = profile.id == state.expandedId,
|
||||||
|
timeSlotB = state.timeSlotB,
|
||||||
|
onRowClick = viewModel::toggleEnabled,
|
||||||
|
onTimeSlotAClick = viewModel::enableForOneHour,
|
||||||
|
onTimeSlotBClick = viewModel::enableUntil,
|
||||||
|
onToggleClick = viewModel::setExpanded,
|
||||||
|
onViewSettingsClick = { navigateToSettings(it) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
customPref(
|
||||||
|
NotificationProfileSelection.New(
|
||||||
|
onClick = {
|
||||||
|
startActivity(AppSettingsActivity.createNotificationProfile(requireContext()))
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(20f).toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToSettings(notificationProfile: NotificationProfile) {
|
||||||
|
startActivity(AppSettingsActivity.notificationProfileDetails(requireContext(), notificationProfile.id))
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(fragmentManager: FragmentManager) {
|
||||||
|
NotificationProfileSelectionFragment().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.manual
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
data class NotificationProfileSelectionState(
|
||||||
|
val notificationProfiles: List<NotificationProfile> = listOf(),
|
||||||
|
val expandedId: Long = -1L,
|
||||||
|
val timeSlotB: Calendar
|
||||||
|
)
|
|
@ -0,0 +1,88 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.manual
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class NotificationProfileSelectionViewModel(private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val store = Store(NotificationProfileSelectionState(timeSlotB = getTimeSlotB()))
|
||||||
|
|
||||||
|
val state: LiveData<NotificationProfileSelectionState> = store.stateLiveData
|
||||||
|
|
||||||
|
val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
init {
|
||||||
|
disposables += repository.getProfiles().subscribeBy(onNext = { profiles -> store.update { it.copy(notificationProfiles = profiles) } })
|
||||||
|
|
||||||
|
disposables += Observable
|
||||||
|
.interval(0, 1, TimeUnit.MINUTES)
|
||||||
|
.map { getTimeSlotB() }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribe { calendar ->
|
||||||
|
store.update { it.copy(timeSlotB = calendar) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
disposables.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setExpanded(notificationProfile: NotificationProfile) {
|
||||||
|
store.update {
|
||||||
|
it.copy(expandedId = if (it.expandedId == notificationProfile.id) -1L else notificationProfile.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleEnabled(profile: NotificationProfile) {
|
||||||
|
disposables += repository.manuallyToggleProfile(profile)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableForOneHour(profile: NotificationProfile) {
|
||||||
|
disposables += repository.manuallyEnableProfileForDuration(profile.id, System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableUntil(profile: NotificationProfile, calendar: Calendar) {
|
||||||
|
disposables += repository.manuallyEnableProfileForDuration(profile.id, calendar.timeInMillis)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun getTimeSlotB(): Calendar {
|
||||||
|
val now = Calendar.getInstance()
|
||||||
|
val sixPm = Calendar.getInstance()
|
||||||
|
val eightAm = Calendar.getInstance()
|
||||||
|
|
||||||
|
sixPm.set(Calendar.HOUR_OF_DAY, 18)
|
||||||
|
sixPm.set(Calendar.MINUTE, 0)
|
||||||
|
sixPm.set(Calendar.SECOND, 0)
|
||||||
|
|
||||||
|
eightAm.set(Calendar.HOUR_OF_DAY, 8)
|
||||||
|
eightAm.set(Calendar.MINUTE, 0)
|
||||||
|
eightAm.set(Calendar.SECOND, 0)
|
||||||
|
|
||||||
|
return if (now.before(sixPm) && (now.after(eightAm) || now == eightAm)) {
|
||||||
|
sixPm
|
||||||
|
} else {
|
||||||
|
eightAm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val notificationProfilesRepository: NotificationProfilesRepository) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(NotificationProfileSelectionViewModel(notificationProfilesRepository))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.manual.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.Group
|
||||||
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification Profile selection preference.
|
||||||
|
*/
|
||||||
|
object NotificationProfileSelection {
|
||||||
|
|
||||||
|
private const val TOGGLE_EXPANSION = 0
|
||||||
|
private const val UPDATE_TIMESLOT = 1
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(New::class.java, MappingAdapter.LayoutFactory(::NewViewHolder, R.layout.new_notification_profile_pref))
|
||||||
|
adapter.registerFactory(Entry::class.java, MappingAdapter.LayoutFactory(::EntryViewHolder, R.layout.notification_profile_entry_pref))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Entry(
|
||||||
|
val isOn: Boolean,
|
||||||
|
override val summary: DSLSettingsText,
|
||||||
|
val notificationProfile: NotificationProfile,
|
||||||
|
val isExpanded: Boolean,
|
||||||
|
val timeSlotB: Calendar,
|
||||||
|
val onRowClick: (NotificationProfile) -> Unit,
|
||||||
|
val onTimeSlotAClick: (NotificationProfile) -> Unit,
|
||||||
|
val onTimeSlotBClick: (NotificationProfile, Calendar) -> Unit,
|
||||||
|
val onViewSettingsClick: (NotificationProfile) -> Unit,
|
||||||
|
val onToggleClick: (NotificationProfile) -> Unit
|
||||||
|
) : PreferenceModel<Entry>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(newItem: Entry): Boolean {
|
||||||
|
return notificationProfile.id == newItem.notificationProfile.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Entry): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) &&
|
||||||
|
isOn == newItem.isOn &&
|
||||||
|
notificationProfile == newItem.notificationProfile &&
|
||||||
|
isExpanded == newItem.isExpanded &&
|
||||||
|
timeSlotB == newItem.timeSlotB
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(newItem: Entry): Any? {
|
||||||
|
return if (notificationProfile == newItem.notificationProfile && isExpanded != newItem.isExpanded) {
|
||||||
|
TOGGLE_EXPANSION
|
||||||
|
} else if (notificationProfile == newItem.notificationProfile && timeSlotB != newItem.timeSlotB) {
|
||||||
|
UPDATE_TIMESLOT
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntryViewHolder(itemView: View) : MappingViewHolder<Entry>(itemView) {
|
||||||
|
|
||||||
|
private val image: EmojiImageView = findViewById(R.id.notification_preference_image)
|
||||||
|
private val chevron: View = findViewById(R.id.notification_preference_chevron)
|
||||||
|
private val name: TextView = findViewById(R.id.notification_preference_name)
|
||||||
|
private val status: TextView = findViewById(R.id.notification_preference_status)
|
||||||
|
private val expansion: Group = findViewById(R.id.notification_preference_expanded)
|
||||||
|
private val timeSlotA: TextView = findViewById(R.id.notification_preference_1hr)
|
||||||
|
private val timeSlotB: TextView = findViewById(R.id.notification_preference_6pm)
|
||||||
|
private val viewSettings: View = findViewById(R.id.notification_preference_view_settings)
|
||||||
|
|
||||||
|
override fun bind(model: Entry) {
|
||||||
|
itemView.setOnClickListener { model.onRowClick(model.notificationProfile) }
|
||||||
|
chevron.setOnClickListener { model.onToggleClick(model.notificationProfile) }
|
||||||
|
chevron.rotation = if (model.isExpanded) 180f else 0f
|
||||||
|
timeSlotA.setOnClickListener { model.onTimeSlotAClick(model.notificationProfile) }
|
||||||
|
timeSlotB.setOnClickListener { model.onTimeSlotBClick(model.notificationProfile, model.timeSlotB) }
|
||||||
|
viewSettings.setOnClickListener { model.onViewSettingsClick(model.notificationProfile) }
|
||||||
|
|
||||||
|
expansion.visible = model.isExpanded
|
||||||
|
timeSlotB.text = context.getString(
|
||||||
|
R.string.NotificationProfileSelection__until_s,
|
||||||
|
DateUtils.getTimeString(context, Locale.getDefault(), model.timeSlotB.timeInMillis)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (TOGGLE_EXPANSION in payload || UPDATE_TIMESLOT in payload) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image.background.colorFilter = SimpleColorFilter(model.notificationProfile.color.colorInt())
|
||||||
|
if (model.notificationProfile.emoji.isNotEmpty()) {
|
||||||
|
image.setImageEmoji(model.notificationProfile.emoji)
|
||||||
|
} else {
|
||||||
|
image.setImageResource(R.drawable.ic_moon_24)
|
||||||
|
}
|
||||||
|
|
||||||
|
name.text = model.notificationProfile.name
|
||||||
|
|
||||||
|
presentStatus(model)
|
||||||
|
|
||||||
|
timeSlotB.text = context.getString(
|
||||||
|
R.string.NotificationProfileSelection__until_s,
|
||||||
|
DateUtils.getTimeString(context, Locale.getDefault(), model.timeSlotB.timeInMillis)
|
||||||
|
)
|
||||||
|
|
||||||
|
itemView.isSelected = model.isOn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun presentStatus(model: Entry) {
|
||||||
|
status.isEnabled = model.isOn
|
||||||
|
status.text = model.summary.resolve(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class New(val onClick: () -> Unit) : PreferenceModel<New>() {
|
||||||
|
override fun areItemsTheSame(newItem: New): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewViewHolder(itemView: View) : MappingViewHolder<New>(itemView) {
|
||||||
|
override fun bind(model: New) {
|
||||||
|
itemView.setOnClickListener { model.onClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.dd.CircularProgressButton
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileAddMembers
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileRecipient
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show and allow addition of recipients to a profile during the create flow.
|
||||||
|
*/
|
||||||
|
class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragment_add_allowed_members) {
|
||||||
|
|
||||||
|
private val viewModel: AddAllowedMembersViewModel by viewModels(factoryProducer = { AddAllowedMembersViewModel.Factory(profileId) })
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
private val profileId: Long by lazy { AddAllowedMembersFragmentArgs.fromBundle(requireArguments()).profileId }
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
|
||||||
|
view.findViewById<CircularProgressButton>(R.id.add_allowed_members_profile_next).apply {
|
||||||
|
setOnClickListener {
|
||||||
|
findNavController().navigate(AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToEditNotificationProfileScheduleFragment(profileId, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
NotificationProfileAddMembers.register(adapter)
|
||||||
|
NotificationProfileRecipient.register(adapter)
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.getProfile()
|
||||||
|
.subscribeBy(
|
||||||
|
onNext = { (profile, recipients) ->
|
||||||
|
adapter.submitList(getConfiguration(profile, recipients).toMappingModelList())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(profile: NotificationProfile, recipients: List<Recipient>): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
sectionHeaderPref(R.string.AddAllowedMembers__allowed_notifications)
|
||||||
|
|
||||||
|
customPref(
|
||||||
|
NotificationProfileAddMembers.Model(
|
||||||
|
onClick = { id, currentSelection ->
|
||||||
|
findNavController().navigate(
|
||||||
|
AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToSelectRecipientsFragment(id)
|
||||||
|
.setCurrentSelection(currentSelection.toTypedArray())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
profileId = profile.id,
|
||||||
|
currentSelection = profile.allowedMembers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (member in recipients) {
|
||||||
|
customPref(
|
||||||
|
NotificationProfileRecipient.Model(
|
||||||
|
recipientModel = RecipientPreference.Model(
|
||||||
|
recipient = member,
|
||||||
|
onClick = {}
|
||||||
|
),
|
||||||
|
onRemoveClick = { id ->
|
||||||
|
lifecycleDisposable += viewModel.removeMember(id)
|
||||||
|
.subscribeBy(
|
||||||
|
onSuccess = { removed ->
|
||||||
|
view?.let { view ->
|
||||||
|
Snackbar.make(view, getString(R.string.NotificationProfileDetails__s_removed, removed.getDisplayName(requireContext())), Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.NotificationProfileDetails__undo) { undoRemove(id) }
|
||||||
|
.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.core_ultramarine_light))
|
||||||
|
.setTextColor(Color.WHITE)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun undoRemove(id: RecipientId) {
|
||||||
|
lifecycleDisposable += viewModel.addMember(id)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
class AddAllowedMembersViewModel(private val profileId: Long, private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||||
|
|
||||||
|
fun getProfile(): Observable<NotificationProfileAndRecipients> {
|
||||||
|
return repository.getProfile(profileId)
|
||||||
|
.map { profile ->
|
||||||
|
NotificationProfileAndRecipients(profile, profile.allowedMembers.map { Recipient.resolved(it) })
|
||||||
|
}
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMember(id: RecipientId): Single<NotificationProfile> {
|
||||||
|
return repository.addMember(profileId, id)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMember(id: RecipientId): Single<Recipient> {
|
||||||
|
return repository.removeMember(profileId, id)
|
||||||
|
.map { Recipient.resolved(id) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NotificationProfileAndRecipients(val profile: NotificationProfile, val recipients: List<Recipient>)
|
||||||
|
|
||||||
|
class Factory(private val profileId: Long) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(AddAllowedMembersViewModel(profileId, NotificationProfilesRepository()))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.dd.CircularProgressButton
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.signal.core.util.BreakIteratorCompat
|
||||||
|
import org.signal.core.util.EditTextUtil
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileViewModel.SaveNotificationProfileResult
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileNamePreset
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||||
|
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.util.text.AfterTextChanged
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dual use Edit/Create notification profile fragment. Use to create in the create profile flow,
|
||||||
|
* and then to edit from profile details. Responsible for naming and emoji.
|
||||||
|
*/
|
||||||
|
class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.fragment_edit_notification_profile), ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||||
|
|
||||||
|
private val viewModel: EditNotificationProfileViewModel by viewModels(factoryProducer = this::createFactory)
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
|
||||||
|
private var emojiView: ImageView? = null
|
||||||
|
private var nameView: EditText? = null
|
||||||
|
|
||||||
|
private fun createFactory(): ViewModelProvider.Factory {
|
||||||
|
val profileId = EditNotificationProfileFragmentArgs.fromBundle(requireArguments()).profileId
|
||||||
|
return EditNotificationProfileViewModel.Factory(profileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.NOTIFICATION_PROFILES)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||||
|
toolbar.setNavigationOnClickListener {
|
||||||
|
ViewUtil.hideKeyboard(requireContext(), requireView())
|
||||||
|
requireActivity().onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
val title: TextView = view.findViewById(R.id.edit_notification_profile_title)
|
||||||
|
val countView: TextView = view.findViewById(R.id.edit_notification_profile_count)
|
||||||
|
val saveButton: CircularProgressButton = view.findViewById(R.id.edit_notification_profile_save)
|
||||||
|
val emojiView: ImageView = view.findViewById(R.id.edit_notification_profile_emoji)
|
||||||
|
val nameView: EditText = view.findViewById(R.id.edit_notification_profile_name)
|
||||||
|
val nameTextWrapper: TextInputLayout = view.findViewById(R.id.edit_notification_profile_name_wrapper)
|
||||||
|
|
||||||
|
EditTextUtil.addGraphemeClusterLimitFilter(nameView, NOTIFICATION_PROFILE_NAME_MAX_GLYPHS)
|
||||||
|
nameView.addTextChangedListener(
|
||||||
|
AfterTextChanged { editable: Editable ->
|
||||||
|
presentCount(countView, editable.toString())
|
||||||
|
nameTextWrapper.error = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
emojiView.setOnClickListener {
|
||||||
|
ReactWithAnyEmojiBottomSheetDialogFragment.createForAboutSelection()
|
||||||
|
.show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<View>(R.id.edit_notification_profile_clear).setOnClickListener {
|
||||||
|
nameView.setText("")
|
||||||
|
onEmojiSelectedInternal("")
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
|
||||||
|
saveButton.setOnClickListener {
|
||||||
|
if (TextUtils.isEmpty(nameView.text)) {
|
||||||
|
nameTextWrapper.error = getString(R.string.EditNotificationProfileFragment__profile_must_have_a_name)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.save(nameView.text.toString())
|
||||||
|
.doOnSubscribe { CircularProgressButtonUtil.setSpinning(saveButton) }
|
||||||
|
.doAfterTerminate { CircularProgressButtonUtil.cancelSpinning(saveButton) }
|
||||||
|
.subscribeBy(
|
||||||
|
onSuccess = { saveResult ->
|
||||||
|
when (saveResult) {
|
||||||
|
is SaveNotificationProfileResult.Success -> {
|
||||||
|
ViewUtil.hideKeyboard(requireContext(), nameView)
|
||||||
|
if (saveResult.createMode) {
|
||||||
|
findNavController().navigate(EditNotificationProfileFragmentDirections.actionEditNotificationProfileFragmentToAddAllowedMembersFragment(saveResult.profile.id))
|
||||||
|
} else {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SaveNotificationProfileResult.DuplicateNameFailure -> {
|
||||||
|
nameTextWrapper.error = getString(R.string.EditNotificationProfileFragment__a_profile_with_this_name_already_exists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.getInitialState()
|
||||||
|
.subscribeBy(
|
||||||
|
onSuccess = { initial ->
|
||||||
|
if (initial.createMode) {
|
||||||
|
saveButton.text = getString(R.string.EditNotificationProfileFragment__next)
|
||||||
|
title.setText(R.string.EditNotificationProfileFragment__name_your_profile)
|
||||||
|
} else {
|
||||||
|
saveButton.text = getString(R.string.EditNotificationProfileFragment__save)
|
||||||
|
title.setText(R.string.EditNotificationProfileFragment__edit_this_profile)
|
||||||
|
}
|
||||||
|
nameView.setText(initial.name)
|
||||||
|
onEmojiSelectedInternal(initial.emoji)
|
||||||
|
|
||||||
|
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(nameView)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.nameView = nameView
|
||||||
|
this.emojiView = emojiView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
NotificationProfileNamePreset.register(adapter)
|
||||||
|
|
||||||
|
val onClick = { preset: NotificationProfileNamePreset.Model ->
|
||||||
|
nameView?.apply {
|
||||||
|
setText(preset.bodyResource)
|
||||||
|
setSelection(length(), length())
|
||||||
|
}
|
||||||
|
onEmojiSelectedInternal(preset.emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.submitList(
|
||||||
|
listOf(
|
||||||
|
NotificationProfileNamePreset.Model("\uD83D\uDCAA", R.string.EditNotificationProfileFragment__work, onClick),
|
||||||
|
NotificationProfileNamePreset.Model("\uD83D\uDE34", R.string.EditNotificationProfileFragment__sleep, onClick),
|
||||||
|
NotificationProfileNamePreset.Model("\uD83D\uDE97", R.string.EditNotificationProfileFragment__driving, onClick),
|
||||||
|
NotificationProfileNamePreset.Model("\uD83D\uDE0A", R.string.EditNotificationProfileFragment__downtime, onClick),
|
||||||
|
NotificationProfileNamePreset.Model("\uD83D\uDCA1", R.string.EditNotificationProfileFragment__focus, onClick),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReactWithAnyEmojiSelected(emoji: String) {
|
||||||
|
onEmojiSelectedInternal(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReactWithAnyEmojiDialogDismissed() = Unit
|
||||||
|
|
||||||
|
private fun presentCount(countView: TextView, profileName: String) {
|
||||||
|
val breakIterator = BreakIteratorCompat.getInstance()
|
||||||
|
breakIterator.setText(profileName)
|
||||||
|
|
||||||
|
val glyphCount = breakIterator.countBreaks()
|
||||||
|
if (glyphCount >= NOTIFICATION_PROFILE_NAME_LIMIT_DISPLAY_THRESHOLD) {
|
||||||
|
countView.visibility = View.VISIBLE
|
||||||
|
countView.text = resources.getString(R.string.EditNotificationProfileFragment__count, glyphCount, NOTIFICATION_PROFILE_NAME_MAX_GLYPHS)
|
||||||
|
} else {
|
||||||
|
countView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEmojiSelectedInternal(emoji: String) {
|
||||||
|
val drawable = EmojiUtil.convertToDrawable(requireContext(), emoji)
|
||||||
|
if (drawable != null) {
|
||||||
|
emojiView?.setImageDrawable(drawable)
|
||||||
|
viewModel.onEmojiSelected(emoji)
|
||||||
|
} else {
|
||||||
|
emojiView?.setImageResource(R.drawable.ic_add_emoji)
|
||||||
|
viewModel.onEmojiSelected("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_PROFILE_NAME_MAX_GLYPHS = 32
|
||||||
|
private const val NOTIFICATION_PROFILE_NAME_LIMIT_DISPLAY_THRESHOLD = 22
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import android.text.style.AbsoluteSizeSpan
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CheckedTextView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.dd.CircularProgressButton
|
||||||
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
|
import com.google.android.material.timepicker.TimeFormat
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleViewModel.SaveScheduleResult
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.util.formatHours
|
||||||
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can edit existing or use during create flow to setup a profile schedule.
|
||||||
|
*/
|
||||||
|
class EditNotificationProfileScheduleFragment : LoggingFragment(R.layout.fragment_edit_notification_profile_schedule) {
|
||||||
|
|
||||||
|
private val viewModel: EditNotificationProfileScheduleViewModel by viewModels(factoryProducer = { EditNotificationProfileScheduleViewModel.Factory(profileId) })
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
|
||||||
|
private val profileId: Long by lazy { EditNotificationProfileScheduleFragmentArgs.fromBundle(requireArguments()).profileId }
|
||||||
|
private val createMode: Boolean by lazy { EditNotificationProfileScheduleFragmentArgs.fromBundle(requireArguments()).createMode }
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||||
|
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
|
||||||
|
val title: View = view.findViewById(R.id.edit_notification_profile_schedule_title)
|
||||||
|
val description: View = view.findViewById(R.id.edit_notification_profile_schedule_description)
|
||||||
|
|
||||||
|
toolbar.title = if (!createMode) getString(R.string.EditNotificationProfileSchedule__schedule) else null
|
||||||
|
title.visible = createMode
|
||||||
|
description.visible = createMode
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
|
||||||
|
val enableToggle: SwitchMaterial = view.findViewById(R.id.edit_notification_profile_schedule_switch)
|
||||||
|
enableToggle.setOnClickListener { viewModel.setEnabled(enableToggle.isChecked) }
|
||||||
|
|
||||||
|
val startTime: TextView = view.findViewById(R.id.edit_notification_profile_schedule_start_time)
|
||||||
|
val endTime: TextView = view.findViewById(R.id.edit_notification_profile_schedule_end_time)
|
||||||
|
|
||||||
|
val next: CircularProgressButton = view.findViewById(R.id.edit_notification_profile_schedule__next)
|
||||||
|
next.setOnClickListener {
|
||||||
|
lifecycleDisposable += viewModel.save(createMode)
|
||||||
|
.subscribeBy(
|
||||||
|
onSuccess = { result ->
|
||||||
|
when (result) {
|
||||||
|
SaveScheduleResult.Success -> {
|
||||||
|
if (createMode) {
|
||||||
|
findNavController().navigate(EditNotificationProfileScheduleFragmentDirections.actionEditNotificationProfileScheduleFragmentToNotificationProfileCreatedFragment(profileId))
|
||||||
|
} else {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SaveScheduleResult.NoDaysSelected -> {
|
||||||
|
Toast.makeText(requireContext(), R.string.EditNotificationProfileSchedule__schedule_must_have_at_least_one_day, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sunday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_sunday)
|
||||||
|
val monday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_monday)
|
||||||
|
val tuesday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_tuesday)
|
||||||
|
val wednesday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_wednesday)
|
||||||
|
val thursday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_thursday)
|
||||||
|
val friday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_friday)
|
||||||
|
val saturday: CheckedTextView = view.findViewById(R.id.edit_notification_profile_schedule_saturday)
|
||||||
|
|
||||||
|
val days: Map<CheckedTextView, DayOfWeek> = mapOf(
|
||||||
|
sunday to DayOfWeek.SUNDAY,
|
||||||
|
monday to DayOfWeek.MONDAY,
|
||||||
|
tuesday to DayOfWeek.TUESDAY,
|
||||||
|
wednesday to DayOfWeek.WEDNESDAY,
|
||||||
|
thursday to DayOfWeek.THURSDAY,
|
||||||
|
friday to DayOfWeek.FRIDAY,
|
||||||
|
saturday to DayOfWeek.SATURDAY
|
||||||
|
)
|
||||||
|
|
||||||
|
days.forEach { (view, day) ->
|
||||||
|
DrawableCompat.setTintList(view.background, ContextCompat.getColorStateList(requireContext(), R.color.notification_profile_schedule_background_tint))
|
||||||
|
view.setOnClickListener { viewModel.toggleDay(day) }
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.schedule()
|
||||||
|
.subscribeBy(
|
||||||
|
onNext = { schedule ->
|
||||||
|
enableToggle.isChecked = schedule.enabled
|
||||||
|
enableToggle.isEnabled = true
|
||||||
|
|
||||||
|
days.forEach { (view, day) ->
|
||||||
|
view.isChecked = schedule.daysEnabled.contains(day)
|
||||||
|
view.isEnabled = schedule.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime.text = schedule.startTime().formatTime()
|
||||||
|
startTime.setOnClickListener { showTimeSelector(true, schedule.startTime()) }
|
||||||
|
startTime.isEnabled = schedule.enabled
|
||||||
|
|
||||||
|
endTime.text = schedule.endTime().formatTime()
|
||||||
|
endTime.setOnClickListener { showTimeSelector(false, schedule.endTime()) }
|
||||||
|
endTime.isEnabled = schedule.enabled
|
||||||
|
|
||||||
|
if (createMode) {
|
||||||
|
next.setText(if (schedule.enabled) R.string.EditNotificationProfileSchedule__next else R.string.EditNotificationProfileSchedule__skip)
|
||||||
|
} else {
|
||||||
|
next.setText(R.string.EditNotificationProfileSchedule__save)
|
||||||
|
}
|
||||||
|
next.isEnabled = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showTimeSelector(isStart: Boolean, time: LocalTime) {
|
||||||
|
val timeFormat = if (DateFormat.is24HourFormat(requireContext())) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H
|
||||||
|
val timePickerFragment = MaterialTimePicker.Builder()
|
||||||
|
.setTimeFormat(timeFormat)
|
||||||
|
.setHour(time.hour)
|
||||||
|
.setMinute(time.minute)
|
||||||
|
.setTitleText(if (isStart) R.string.EditNotificationProfileSchedule__set_start_time else R.string.EditNotificationProfileSchedule__set_end_time)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
timePickerFragment.addOnDismissListener {
|
||||||
|
timePickerFragment.clearOnDismissListeners()
|
||||||
|
timePickerFragment.clearOnPositiveButtonClickListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
timePickerFragment.addOnPositiveButtonClickListener {
|
||||||
|
val hour = timePickerFragment.hour
|
||||||
|
val minute = timePickerFragment.minute
|
||||||
|
|
||||||
|
if (isStart) {
|
||||||
|
viewModel.setStartTime(hour, minute)
|
||||||
|
} else {
|
||||||
|
viewModel.setEndTime(hour, minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timePickerFragment.show(childFragmentManager, "TIME_PICKER")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LocalTime.formatTime(): SpannableString {
|
||||||
|
val amPm = DateTimeFormatter.ofPattern("a")
|
||||||
|
.format(this)
|
||||||
|
|
||||||
|
val formattedTime: String = this.formatHours()
|
||||||
|
|
||||||
|
return SpannableString(formattedTime).apply {
|
||||||
|
val amPmIndex = formattedTime.indexOf(string = amPm, ignoreCase = true)
|
||||||
|
if (amPmIndex != -1) {
|
||||||
|
setSpan(AbsoluteSizeSpan(ViewUtil.spToPx(20f)), amPmIndex, amPmIndex + amPm.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for driving edit schedule UI. UI starts in a disabled state until the first schedule is loaded
|
||||||
|
* from the database and into the [scheduleSubject] allowing the safe use of !! with [schedule].
|
||||||
|
*/
|
||||||
|
class EditNotificationProfileScheduleViewModel(
|
||||||
|
profileId: Long,
|
||||||
|
private val repository: NotificationProfilesRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
private val scheduleSubject: BehaviorSubject<NotificationProfileSchedule> = BehaviorSubject.create()
|
||||||
|
private val schedule: NotificationProfileSchedule
|
||||||
|
get() = scheduleSubject.value!!
|
||||||
|
|
||||||
|
init {
|
||||||
|
disposables += repository.getProfile(profileId)
|
||||||
|
.take(1)
|
||||||
|
.map { it.schedule }
|
||||||
|
.singleOrError()
|
||||||
|
.subscribeBy(onSuccess = { scheduleSubject.onNext(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
disposables.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun schedule(): Observable<NotificationProfileSchedule> {
|
||||||
|
return scheduleSubject.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleDay(day: DayOfWeek) {
|
||||||
|
val newDaysEnabled = schedule.daysEnabled.toMutableSet()
|
||||||
|
if (newDaysEnabled.contains(day)) {
|
||||||
|
newDaysEnabled.remove(day)
|
||||||
|
} else {
|
||||||
|
newDaysEnabled += day
|
||||||
|
}
|
||||||
|
scheduleSubject.onNext(schedule.copy(daysEnabled = newDaysEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStartTime(hour: Int, minute: Int) {
|
||||||
|
scheduleSubject.onNext(schedule.copy(start = hour * 100 + minute))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEndTime(hour: Int, minute: Int) {
|
||||||
|
scheduleSubject.onNext(schedule.copy(end = hour * 100 + minute))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEnabled(enabled: Boolean) {
|
||||||
|
scheduleSubject.onNext(schedule.copy(enabled = enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(createMode: Boolean): Single<SaveScheduleResult> {
|
||||||
|
val result: Single<SaveScheduleResult> = if (schedule.enabled && schedule.daysEnabled.isEmpty()) {
|
||||||
|
Single.just(SaveScheduleResult.NoDaysSelected)
|
||||||
|
} else if (createMode && !schedule.enabled) {
|
||||||
|
Single.just(SaveScheduleResult.Success)
|
||||||
|
} else {
|
||||||
|
repository.updateSchedule(schedule).toSingle { SaveScheduleResult.Success }
|
||||||
|
}
|
||||||
|
return result.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val profileId: Long) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(EditNotificationProfileScheduleViewModel(profileId, NotificationProfilesRepository()))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SaveScheduleResult {
|
||||||
|
NoDaysSelected,
|
||||||
|
Success
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileDatabase
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
|
||||||
|
class EditNotificationProfileViewModel(private val profileId: Long, private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val createMode: Boolean = profileId == -1L
|
||||||
|
private var selectedEmoji: String = ""
|
||||||
|
|
||||||
|
fun getInitialState(): Single<InitialState> {
|
||||||
|
val initialState = if (createMode) {
|
||||||
|
Single.just(InitialState(createMode))
|
||||||
|
} else {
|
||||||
|
repository.getProfile(profileId)
|
||||||
|
.take(1)
|
||||||
|
.map { InitialState(createMode, it.name, it.emoji) }
|
||||||
|
.singleOrError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialState.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEmojiSelected(emoji: String) {
|
||||||
|
selectedEmoji = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(name: String): Single<SaveNotificationProfileResult> {
|
||||||
|
val save = if (createMode) repository.createProfile(name, selectedEmoji) else repository.updateProfile(profileId, name, selectedEmoji)
|
||||||
|
|
||||||
|
return save.map { r ->
|
||||||
|
when (r) {
|
||||||
|
is NotificationProfileDatabase.NotificationProfileChangeResult.Success -> SaveNotificationProfileResult.Success(r.notificationProfile, createMode)
|
||||||
|
NotificationProfileDatabase.NotificationProfileChangeResult.DuplicateName -> SaveNotificationProfileResult.DuplicateNameFailure
|
||||||
|
}
|
||||||
|
}.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val profileId: Long) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(EditNotificationProfileViewModel(profileId, NotificationProfilesRepository()))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InitialState(
|
||||||
|
val createMode: Boolean,
|
||||||
|
val name: String = "",
|
||||||
|
val emoji: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class SaveNotificationProfileResult {
|
||||||
|
data class Success(val profile: NotificationProfile, val createMode: Boolean) : SaveNotificationProfileResult()
|
||||||
|
object DuplicateNameFailure : SaveNotificationProfileResult()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown at the end of the profile create flow.
|
||||||
|
*/
|
||||||
|
class NotificationProfileCreatedFragment : LoggingFragment(R.layout.fragment_notification_profile_created) {
|
||||||
|
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
private val profileId: Long by lazy { NotificationProfileCreatedFragmentArgs.fromBundle(requireArguments()).profileId }
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val topIcon: ImageView = view.findViewById(R.id.notification_profile_created_top_image)
|
||||||
|
val topText: TextView = view.findViewById(R.id.notification_profile_created_top_text)
|
||||||
|
val bottomIcon: ImageView = view.findViewById(R.id.notification_profile_created_bottom_image)
|
||||||
|
val bottomText: TextView = view.findViewById(R.id.notification_profile_created_bottom_text)
|
||||||
|
|
||||||
|
view.findViewById<View>(R.id.notification_profile_created_done).setOnClickListener {
|
||||||
|
findNavController().navigate(NotificationProfileCreatedFragmentDirections.actionNotificationProfileCreatedFragmentToNotificationProfileDetailsFragment(profileId))
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
|
||||||
|
val repository = NotificationProfilesRepository()
|
||||||
|
lifecycleDisposable += repository.getProfile(profileId)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeBy(
|
||||||
|
onNext = { profile ->
|
||||||
|
if (profile.schedule.enabled) {
|
||||||
|
topIcon.setImageResource(R.drawable.ic_recent_20)
|
||||||
|
topText.setText(R.string.NotificationProfileCreated__your_profile_will_turn_on_and_off_automatically_according_to_your_schedule)
|
||||||
|
|
||||||
|
bottomIcon.setImageResource(R.drawable.ic_more_vert_24)
|
||||||
|
bottomText.setText(R.string.NotificationProfileCreated__you_can_turn_your_profile_on_or_off_manually_via_the_menu_on_the_chat_list)
|
||||||
|
} else {
|
||||||
|
topIcon.setImageResource(R.drawable.ic_more_vert_24)
|
||||||
|
topText.setText(R.string.NotificationProfileCreated__you_can_turn_your_profile_on_or_off_manually_via_the_menu_on_the_chat_list)
|
||||||
|
|
||||||
|
bottomIcon.setImageResource(R.drawable.ic_recent_20)
|
||||||
|
bottomText.setText(R.string.NotificationProfileCreated__add_a_schedule_in_settings_to_automate_your_profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileAddMembers
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfilePreference
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileRecipient
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
|
import org.thoughtcrime.securesms.util.formatHours
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
private val DAY_ORDER: List<DayOfWeek> = listOf(DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY)
|
||||||
|
|
||||||
|
class NotificationProfileDetailsFragment : DSLSettingsFragment() {
|
||||||
|
|
||||||
|
private val viewModel: NotificationProfileDetailsViewModel by viewModels(factoryProducer = this::createFactory)
|
||||||
|
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
|
private fun createFactory(): ViewModelProvider.Factory {
|
||||||
|
return NotificationProfileDetailsViewModel.Factory(NotificationProfileDetailsFragmentArgs.fromBundle(requireArguments()).profileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
toolbar = view.findViewById(R.id.toolbar)
|
||||||
|
toolbar?.inflateMenu(R.menu.notification_profile_details)
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
toolbar = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
NotificationProfilePreference.register(adapter)
|
||||||
|
NotificationProfileAddMembers.register(adapter)
|
||||||
|
NotificationProfileRecipient.register(adapter)
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.getProfile()
|
||||||
|
.subscribeBy(
|
||||||
|
onNext = { state ->
|
||||||
|
when (state) {
|
||||||
|
is NotificationProfileDetailsViewModel.State.Valid -> {
|
||||||
|
toolbar?.title = state.profile.name
|
||||||
|
toolbar?.setOnMenuItemClickListener { item ->
|
||||||
|
if (item.itemId == R.id.action_edit) {
|
||||||
|
findNavController().navigate(NotificationProfileDetailsFragmentDirections.actionNotificationProfileDetailsFragmentToEditNotificationProfileFragment().setProfileId(state.profile.id))
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.submitList(getConfiguration(state.profile, state.recipients, state.isOn).toMappingModelList())
|
||||||
|
}
|
||||||
|
NotificationProfileDetailsViewModel.State.Invalid -> findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(profile: NotificationProfile, recipients: List<Recipient>, isOn: Boolean): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
|
||||||
|
customPref(
|
||||||
|
NotificationProfilePreference.Model(
|
||||||
|
title = DSLSettingsText.from(profile.name),
|
||||||
|
summary = if (isOn) DSLSettingsText.from(NotificationProfiles.getActiveProfileDescription(requireContext(), profile)) else null,
|
||||||
|
icon = if (profile.emoji.isNotEmpty()) EmojiUtil.convertToDrawable(requireContext(), profile.emoji)?.let { DSLSettingsIcon.from(it) } else DSLSettingsIcon.from(R.drawable.ic_moon_24, NO_TINT),
|
||||||
|
color = profile.color,
|
||||||
|
isOn = isOn,
|
||||||
|
showSwitch = true,
|
||||||
|
onClick = {
|
||||||
|
lifecycleDisposable += viewModel.toggleEnabled(profile)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.AddAllowedMembers__allowed_notifications)
|
||||||
|
customPref(
|
||||||
|
NotificationProfileAddMembers.Model(
|
||||||
|
onClick = { id, currentSelection ->
|
||||||
|
findNavController().navigate(
|
||||||
|
NotificationProfileDetailsFragmentDirections.actionNotificationProfileDetailsFragmentToSelectRecipientsFragment(id)
|
||||||
|
.setCurrentSelection(currentSelection.toTypedArray())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
profileId = profile.id,
|
||||||
|
currentSelection = profile.allowedMembers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for (member in recipients) {
|
||||||
|
customPref(
|
||||||
|
NotificationProfileRecipient.Model(
|
||||||
|
recipientModel = RecipientPreference.Model(
|
||||||
|
recipient = member
|
||||||
|
),
|
||||||
|
onRemoveClick = { id ->
|
||||||
|
lifecycleDisposable += viewModel.removeMember(id)
|
||||||
|
.subscribeBy(
|
||||||
|
onSuccess = { removed ->
|
||||||
|
view?.let { view ->
|
||||||
|
Snackbar.make(view, getString(R.string.NotificationProfileDetails__s_removed, removed.getDisplayName(requireContext())), Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.NotificationProfileDetails__undo) { undoRemove(id) }
|
||||||
|
.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.core_ultramarine_light))
|
||||||
|
.setTextColor(Color.WHITE)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
sectionHeaderPref(R.string.NotificationProfileDetails__schedule)
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(profile.schedule.describe()),
|
||||||
|
summary = DSLSettingsText.from(if (profile.schedule.enabled) R.string.NotificationProfileDetails__on else R.string.NotificationProfileDetails__off),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_recent_20, NO_TINT),
|
||||||
|
onClick = {
|
||||||
|
findNavController().navigate(NotificationProfileDetailsFragmentDirections.actionNotificationProfileDetailsFragmentToEditNotificationProfileScheduleFragment(profile.id, false))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
sectionHeaderPref(R.string.NotificationProfileDetails__exceptions)
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.NotificationProfileDetails__allow_all_calls),
|
||||||
|
isChecked = profile.allowAllCalls,
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_phone_right_24),
|
||||||
|
onClick = {
|
||||||
|
lifecycleDisposable += viewModel.toggleAllowAllCalls()
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
switchPref(
|
||||||
|
title = DSLSettingsText.from(R.string.NotificationProfileDetails__notify_for_all_mentions),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_at_24),
|
||||||
|
isChecked = profile.allowAllMentions,
|
||||||
|
onClick = {
|
||||||
|
lifecycleDisposable += viewModel.toggleAllowAllMentions()
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.NotificationProfileDetails__delete_profile, ContextCompat.getColor(requireContext(), R.color.signal_alert_primary)),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_delete_24, R.color.signal_alert_primary),
|
||||||
|
onClick = {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(R.string.NotificationProfileDetails__permanently_delete_profile)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(
|
||||||
|
SpanUtil.color(
|
||||||
|
ContextCompat.getColor(requireContext(), R.color.signal_alert_primary),
|
||||||
|
getString(R.string.NotificationProfileDetails__delete)
|
||||||
|
)
|
||||||
|
) { _, _ -> deleteProfile() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteProfile() {
|
||||||
|
lifecycleDisposable += viewModel.deleteProfile()
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun undoRemove(id: RecipientId) {
|
||||||
|
lifecycleDisposable += viewModel.addMember(id)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationProfileSchedule.describe(): String {
|
||||||
|
if (!enabled) {
|
||||||
|
return getString(R.string.NotificationProfileDetails__schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
val startTime = startTime().formatHours()
|
||||||
|
val endTime = endTime().formatHours()
|
||||||
|
|
||||||
|
val days = StringBuilder()
|
||||||
|
if (daysEnabled.size == 7) {
|
||||||
|
days.append(getString(R.string.NotificationProfileDetails__everyday))
|
||||||
|
} else {
|
||||||
|
for (day in DAY_ORDER) {
|
||||||
|
if (daysEnabled.contains(day)) {
|
||||||
|
if (days.isNotEmpty()) {
|
||||||
|
days.append(", ")
|
||||||
|
}
|
||||||
|
days.append(day.getDisplayName(TextStyle.SHORT, Locale.getDefault()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getString(R.string.NotificationProfileDetails__s_to_s, startTime, endTime).let { hours ->
|
||||||
|
if (days.isNotEmpty()) "$hours\n$days" else hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Completable
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileDatabase
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
class NotificationProfileDetailsViewModel(private val profileId: Long, private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||||
|
|
||||||
|
fun getProfile(): Observable<State> {
|
||||||
|
return repository.getProfiles()
|
||||||
|
.map { profiles ->
|
||||||
|
val profile = profiles.firstOrNull { it.id == profileId }
|
||||||
|
if (profile == null) {
|
||||||
|
State.Invalid
|
||||||
|
} else {
|
||||||
|
State.Valid(
|
||||||
|
profile = profile,
|
||||||
|
recipients = profile.allowedMembers.map { Recipient.resolved(it) },
|
||||||
|
isOn = NotificationProfiles.getActiveProfile(profiles) == profile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMember(id: RecipientId): Single<NotificationProfile> {
|
||||||
|
return repository.addMember(profileId, id)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMember(id: RecipientId): Single<Recipient> {
|
||||||
|
return repository.removeMember(profileId, id)
|
||||||
|
.map { Recipient.resolved(id) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteProfile(): Completable {
|
||||||
|
return repository.deleteProfile(profileId)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleEnabled(profile: NotificationProfile): Completable {
|
||||||
|
return repository.manuallyToggleProfile(profile)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAllowAllMentions(): Single<NotificationProfile> {
|
||||||
|
return repository.getProfile(profileId)
|
||||||
|
.take(1)
|
||||||
|
.singleOrError()
|
||||||
|
.flatMap { repository.updateProfile(it.copy(allowAllMentions = !it.allowAllMentions)) }
|
||||||
|
.map { (it as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAllowAllCalls(): Single<NotificationProfile> {
|
||||||
|
return repository.getProfile(profileId)
|
||||||
|
.take(1)
|
||||||
|
.singleOrError()
|
||||||
|
.flatMap { repository.updateProfile(it.copy(allowAllCalls = !it.allowAllCalls)) }
|
||||||
|
.map { (it as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class State {
|
||||||
|
data class Valid(
|
||||||
|
val profile: NotificationProfile,
|
||||||
|
val recipients: List<Recipient>,
|
||||||
|
val isOn: Boolean
|
||||||
|
) : State()
|
||||||
|
object Invalid : State()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val profileId: Long) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(NotificationProfileDetailsViewModel(profileId, NotificationProfilesRepository()))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NoNotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfilePreference
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LargeIconClickPreference
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary entry point for Notification Profiles. When user has no profiles, shows empty state, otherwise shows
|
||||||
|
* all current profiles.
|
||||||
|
*/
|
||||||
|
class NotificationProfilesFragment : DSLSettingsFragment() {
|
||||||
|
|
||||||
|
private val viewModel: NotificationProfilesViewModel by viewModels(
|
||||||
|
factoryProducer = { NotificationProfilesViewModel.Factory() }
|
||||||
|
)
|
||||||
|
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.NOTIFICATION_PROFILES)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
toolbar = view.findViewById(R.id.toolbar)
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
toolbar = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
NoNotificationProfiles.register(adapter)
|
||||||
|
LargeIconClickPreference.register(adapter)
|
||||||
|
NotificationProfilePreference.register(adapter)
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.getProfiles()
|
||||||
|
.subscribe { profiles ->
|
||||||
|
if (profiles.isEmpty()) {
|
||||||
|
toolbar?.title = ""
|
||||||
|
} else {
|
||||||
|
toolbar?.setTitle(R.string.NotificationsSettingsFragment__notification_profiles)
|
||||||
|
}
|
||||||
|
adapter.submitList(getConfiguration(profiles).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(profiles: List<NotificationProfile>): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
if (profiles.isEmpty()) {
|
||||||
|
customPref(
|
||||||
|
NoNotificationProfiles.Model(
|
||||||
|
onClick = { findNavController().navigate(R.id.action_notificationProfilesFragment_to_editNotificationProfileFragment) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sectionHeaderPref(R.string.NotificationProfilesFragment__profiles)
|
||||||
|
|
||||||
|
customPref(
|
||||||
|
LargeIconClickPreference.Model(
|
||||||
|
title = DSLSettingsText.from(R.string.NotificationProfilesFragment__new_profile),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
|
||||||
|
onClick = { findNavController().navigate(R.id.action_notificationProfilesFragment_to_editNotificationProfileFragment) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val activeProfile: NotificationProfile? = NotificationProfiles.getActiveProfile(profiles)
|
||||||
|
for (profile: NotificationProfile in profiles) {
|
||||||
|
customPref(
|
||||||
|
NotificationProfilePreference.Model(
|
||||||
|
title = DSLSettingsText.from(profile.name),
|
||||||
|
summary = if (profile == activeProfile) DSLSettingsText.from(NotificationProfiles.getActiveProfileDescription(requireContext(), profile)) else null,
|
||||||
|
icon = if (profile.emoji.isNotEmpty()) EmojiUtil.convertToDrawable(requireContext(), profile.emoji)?.let { DSLSettingsIcon.from(it) } else DSLSettingsIcon.from(R.drawable.ic_moon_24, NO_TINT),
|
||||||
|
color = profile.color,
|
||||||
|
onClick = {
|
||||||
|
findNavController().navigate(NotificationProfilesFragmentDirections.actionNotificationProfilesFragmentToNotificationProfileDetailsFragment(profile.id))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Completable
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.ObservableEmitter
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One stop shop for all your Notification Profile data needs.
|
||||||
|
*/
|
||||||
|
class NotificationProfilesRepository {
|
||||||
|
private val database: NotificationProfileDatabase = SignalDatabase.notificationProfiles
|
||||||
|
|
||||||
|
fun getProfiles(): Observable<List<NotificationProfile>> {
|
||||||
|
return Observable.create { emitter: ObservableEmitter<List<NotificationProfile>> ->
|
||||||
|
val databaseObserver: DatabaseObserver = ApplicationDependencies.getDatabaseObserver()
|
||||||
|
val profileObserver = DatabaseObserver.Observer { emitter.onNext(database.getProfiles()) }
|
||||||
|
|
||||||
|
databaseObserver.registerNotificationProfileObserver(profileObserver)
|
||||||
|
|
||||||
|
emitter.setCancellable { databaseObserver.unregisterObserver(profileObserver) }
|
||||||
|
emitter.onNext(database.getProfiles())
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfile(profileId: Long): Observable<NotificationProfile> {
|
||||||
|
return Observable.create { emitter: ObservableEmitter<NotificationProfile> ->
|
||||||
|
val emitProfile: () -> Unit = {
|
||||||
|
val profile: NotificationProfile? = database.getProfile(profileId)
|
||||||
|
if (profile != null) {
|
||||||
|
emitter.onNext(profile)
|
||||||
|
} else {
|
||||||
|
emitter.onError(NotificationProfileNotFoundException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val databaseObserver: DatabaseObserver = ApplicationDependencies.getDatabaseObserver()
|
||||||
|
val profileObserver = DatabaseObserver.Observer { emitProfile() }
|
||||||
|
|
||||||
|
databaseObserver.registerNotificationProfileObserver(profileObserver)
|
||||||
|
|
||||||
|
emitter.setCancellable { databaseObserver.unregisterObserver(profileObserver) }
|
||||||
|
emitProfile()
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createProfile(name: String, selectedEmoji: String): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
|
||||||
|
return Single.fromCallable { database.createProfile(name = name, emoji = selectedEmoji, color = AvatarColor.random(), createdAt = System.currentTimeMillis()) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProfile(profileId: Long, name: String, selectedEmoji: String): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
|
||||||
|
return Single.fromCallable { database.updateProfile(profileId = profileId, name = name, emoji = selectedEmoji) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
|
||||||
|
return Single.fromCallable { database.updateProfile(profile) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAllowedMembers(profileId: Long, recipients: Set<RecipientId>): Single<NotificationProfile> {
|
||||||
|
return Single.fromCallable { database.setAllowedRecipients(profileId, recipients) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMember(profileId: Long, recipientId: RecipientId): Single<NotificationProfile> {
|
||||||
|
return Single.fromCallable { database.removeAllowedRecipient(profileId, recipientId) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMember(profileId: Long, recipientId: RecipientId): Single<NotificationProfile> {
|
||||||
|
return Single.fromCallable { database.addAllowedRecipient(profileId, recipientId) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteProfile(profileId: Long): Completable {
|
||||||
|
return Completable.fromCallable { database.deleteProfile(profileId) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSchedule(schedule: NotificationProfileSchedule): Completable {
|
||||||
|
return Completable.fromCallable { database.updateSchedule(schedule) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manuallyToggleProfile(profile: NotificationProfile, now: Long = System.currentTimeMillis()): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
val profiles = database.getProfiles()
|
||||||
|
val activeProfile = NotificationProfiles.getActiveProfile(profiles, now)
|
||||||
|
|
||||||
|
if (profile.id == activeProfile?.id) {
|
||||||
|
SignalStore.notificationProfileValues().manuallyEnabledProfile = 0
|
||||||
|
SignalStore.notificationProfileValues().manuallyEnabledUntil = 0
|
||||||
|
SignalStore.notificationProfileValues().manuallyDisabledAt = now
|
||||||
|
SignalStore.notificationProfileValues().lastProfilePopup = 0
|
||||||
|
SignalStore.notificationProfileValues().lastProfilePopupTime = 0
|
||||||
|
} else {
|
||||||
|
val inScheduledWindow = profile.schedule.isCurrentlyActive(now)
|
||||||
|
SignalStore.notificationProfileValues().manuallyEnabledProfile = if (inScheduledWindow) 0 else profile.id
|
||||||
|
SignalStore.notificationProfileValues().manuallyEnabledUntil = if (inScheduledWindow) 0 else Long.MAX_VALUE
|
||||||
|
SignalStore.notificationProfileValues().manuallyDisabledAt = if (inScheduledWindow) 0 else now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnComplete { ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manuallyEnableProfileForDuration(profileId: Long, enableUntil: Long, now: Long = System.currentTimeMillis()): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
SignalStore.notificationProfileValues().manuallyEnabledProfile = profileId
|
||||||
|
SignalStore.notificationProfileValues().manuallyEnabledUntil = enableUntil
|
||||||
|
SignalStore.notificationProfileValues().manuallyDisabledAt = now
|
||||||
|
}
|
||||||
|
.doOnComplete { ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationProfileNotFoundException : Throwable()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
|
||||||
|
class NotificationProfilesViewModel(private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||||
|
|
||||||
|
fun getProfiles(): Observable<List<NotificationProfile>> {
|
||||||
|
return repository.getProfiles()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory() : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(NotificationProfilesViewModel(NotificationProfilesRepository()))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.dd.CircularProgressButton
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||||
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||||
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||||
|
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact Selection for adding recipients to a Notification Profile.
|
||||||
|
*/
|
||||||
|
class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment.OnContactSelectedListener {
|
||||||
|
|
||||||
|
private val viewModel: SelectRecipientsViewModel by viewModels(factoryProducer = this::createFactory)
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
|
||||||
|
private var addToProfile: CircularProgressButton? = null
|
||||||
|
|
||||||
|
private fun createFactory(): ViewModelProvider.Factory {
|
||||||
|
val args = SelectRecipientsFragmentArgs.fromBundle(requireArguments())
|
||||||
|
return SelectRecipientsViewModel.Factory(args.profileId, args.currentSelection?.toSet() ?: emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val currentSelection: Array<RecipientId>? = SelectRecipientsFragmentArgs.fromBundle(requireArguments()).currentSelection
|
||||||
|
val selectionList = ArrayList<RecipientId>()
|
||||||
|
if (currentSelection != null) {
|
||||||
|
selectionList.addAll(currentSelection)
|
||||||
|
}
|
||||||
|
|
||||||
|
childFragmentManager.addFragmentOnAttachListener { _, fragment ->
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putInt(ContactSelectionListFragment.DISPLAY_MODE, getDefaultDisplayMode())
|
||||||
|
putBoolean(ContactSelectionListFragment.REFRESHABLE, false)
|
||||||
|
putBoolean(ContactSelectionListFragment.RECENTS, false)
|
||||||
|
putParcelable(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS)
|
||||||
|
putParcelableArrayList(ContactSelectionListFragment.CURRENT_SELECTION, selectionList)
|
||||||
|
putBoolean(ContactSelectionListFragment.HIDE_COUNT, true)
|
||||||
|
putBoolean(ContactSelectionListFragment.DISPLAY_CHIPS, false)
|
||||||
|
putBoolean(ContactSelectionListFragment.CAN_SELECT_SELF, false)
|
||||||
|
putBoolean(ContactSelectionListFragment.RV_CLIP, false)
|
||||||
|
putInt(ContactSelectionListFragment.RV_PADDING_BOTTOM, ViewUtil.dpToPx(60))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inflater.inflate(R.layout.fragment_select_recipients_fragment, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||||
|
toolbar.setTitle(R.string.AddAllowedMembers__allowed_notifications)
|
||||||
|
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
|
|
||||||
|
val contactFilterView: ContactFilterView = view.findViewById(R.id.contact_filter_edit_text)
|
||||||
|
|
||||||
|
val selectionFragment: ContactSelectionListFragment = childFragmentManager.findFragmentById(R.id.contact_selection_list_fragment) as ContactSelectionListFragment
|
||||||
|
|
||||||
|
contactFilterView.setOnFilterChangedListener {
|
||||||
|
if (it.isNullOrEmpty()) {
|
||||||
|
selectionFragment.resetQueryFilter()
|
||||||
|
} else {
|
||||||
|
selectionFragment.setQueryFilter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToProfile = view.findViewById(R.id.select_recipients_add)
|
||||||
|
addToProfile?.setOnClickListener {
|
||||||
|
lifecycleDisposable += viewModel.updateAllowedMembers()
|
||||||
|
.doOnSubscribe { CircularProgressButtonUtil.setSpinning(addToProfile) }
|
||||||
|
.doOnTerminate { CircularProgressButtonUtil.cancelSpinning(addToProfile) }
|
||||||
|
.subscribeBy(onSuccess = { findNavController().navigateUp() })
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAddToProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
addToProfile = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDefaultDisplayMode(): Int {
|
||||||
|
var mode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or
|
||||||
|
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS or
|
||||||
|
ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW or
|
||||||
|
ContactsCursorLoader.DisplayMode.FLAG_HIDE_RECENT_HEADER
|
||||||
|
|
||||||
|
if (Util.isDefaultSmsProvider(requireContext())) {
|
||||||
|
mode = mode or ContactsCursorLoader.DisplayMode.FLAG_SMS
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode or ContactsCursorLoader.DisplayMode.FLAG_HIDE_GROUPS_V1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeforeContactSelected(recipientId: Optional<RecipientId>, number: String, callback: Consumer<Boolean>) {
|
||||||
|
if (recipientId.isPresent) {
|
||||||
|
viewModel.select(recipientId.get())
|
||||||
|
callback.accept(true)
|
||||||
|
updateAddToProfile()
|
||||||
|
} else {
|
||||||
|
callback.accept(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContactDeselected(recipientId: Optional<RecipientId>, number: String) {
|
||||||
|
if (recipientId.isPresent) {
|
||||||
|
viewModel.deselect(recipientId.get())
|
||||||
|
updateAddToProfile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectionChanged() = Unit
|
||||||
|
|
||||||
|
private fun updateAddToProfile() {
|
||||||
|
val enabled = viewModel.recipients.isNotEmpty()
|
||||||
|
addToProfile?.isEnabled = enabled
|
||||||
|
addToProfile?.alpha = if (enabled) 1f else 0.5f
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
class SelectRecipientsViewModel(
|
||||||
|
private val profileId: Long,
|
||||||
|
currentSelection: Set<RecipientId>,
|
||||||
|
private val repository: NotificationProfilesRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val recipients: MutableSet<RecipientId> = currentSelection.toMutableSet()
|
||||||
|
|
||||||
|
fun select(recipientId: RecipientId) {
|
||||||
|
recipients += recipientId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deselect(recipientId: RecipientId) {
|
||||||
|
recipients.remove(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAllowedMembers(): Single<NotificationProfile> {
|
||||||
|
return repository.updateAllowedMembers(profileId, recipients)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val profileId: Long, val currentSelection: Set<RecipientId>) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(SelectRecipientsViewModel(profileId, currentSelection, NotificationProfilesRepository()))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL custom preference for showing no profiles/empty state.
|
||||||
|
*/
|
||||||
|
object NoNotificationProfiles {
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.notification_profiles_empty))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Model(val onClick: () -> Unit) : PreferenceModel<Model>() {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val icon: ImageView = findViewById(R.id.notification_profiles_empty_icon)
|
||||||
|
private val button: View = findViewById(R.id.notification_profiles_empty_create_profile)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
icon.background.colorFilter = SimpleColorFilter(AvatarColor.A100.colorInt())
|
||||||
|
button.setOnClickListener { model.onClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom DSL preference for adding members to a profile.
|
||||||
|
*/
|
||||||
|
object NotificationProfileAddMembers {
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.large_icon_preference_item))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Model(
|
||||||
|
override val title: DSLSettingsText = DSLSettingsText.from(R.string.AddAllowedMembers__add_people_or_groups),
|
||||||
|
override val icon: DSLSettingsIcon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
|
||||||
|
val onClick: (Long, Set<RecipientId>) -> Unit,
|
||||||
|
val profileId: Long,
|
||||||
|
val currentSelection: Set<RecipientId>
|
||||||
|
) : PreferenceModel<Model>() {
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && profileId == newItem.profileId && currentSelection == newItem.currentSelection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewHolder(itemView: View) : PreferenceViewHolder<Model>(itemView) {
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
super.bind(model)
|
||||||
|
itemView.setOnClickListener { model.onClick(model.profileId, model.currentSelection) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL custom preference for showing default emoji/name combos for create/edit profile.
|
||||||
|
*/
|
||||||
|
object NotificationProfileNamePreset {
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.about_preset_item))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Model(val emoji: String, @StringRes val bodyResource: Int, val onClick: (Model) -> Unit) : MappingModel<Model> {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
return bodyResource == newItem.bodyResource
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return areItemsTheSame(newItem) && emoji == newItem.emoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
val emoji: ImageView = findViewById(R.id.about_preset_emoji)
|
||||||
|
val body: TextView = findViewById(R.id.about_preset_body)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
itemView.setOnClickListener { model.onClick(model) }
|
||||||
|
emoji.setImageDrawable(EmojiUtil.convertToDrawable(context, model.emoji))
|
||||||
|
body.setText(model.bodyResource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL custom preference for showing Notification Profile rows.
|
||||||
|
*/
|
||||||
|
object NotificationProfilePreference {
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.notification_profile_preference_item))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Model(
|
||||||
|
override val title: DSLSettingsText,
|
||||||
|
override val summary: DSLSettingsText?,
|
||||||
|
override val icon: DSLSettingsIcon?,
|
||||||
|
val color: AvatarColor,
|
||||||
|
val isOn: Boolean = false,
|
||||||
|
val showSwitch: Boolean = false,
|
||||||
|
val onClick: () -> Unit
|
||||||
|
) : PreferenceModel<Model>()
|
||||||
|
|
||||||
|
private class ViewHolder(itemView: View) : PreferenceViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val switchWidget: SwitchMaterial = itemView.findViewById(R.id.switch_widget)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
super.bind(model)
|
||||||
|
itemView.setOnClickListener { model.onClick() }
|
||||||
|
switchWidget.visible = model.showSwitch
|
||||||
|
switchWidget.isEnabled = model.isEnabled
|
||||||
|
switchWidget.isChecked = model.isOn
|
||||||
|
iconView.background.colorFilter = SimpleColorFilter(model.color.colorInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL custom preference for showing recipients in a profile. Delegates most work to [RecipientPreference].
|
||||||
|
*/
|
||||||
|
object NotificationProfileRecipient {
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.notification_profile_recipient_list_item))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Model(val recipientModel: RecipientPreference.Model, val onRemoveClick: (RecipientId) -> Unit) : PreferenceModel<Model>() {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
return recipientModel.recipient.id == newItem.recipientModel.recipient.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && recipientModel.areContentsTheSame(newItem.recipientModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val recipientViewHolder: RecipientPreference.ViewHolder = RecipientPreference.ViewHolder(itemView)
|
||||||
|
private val remove: View = findViewById(R.id.recipient_remove)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
recipientViewHolder.bind(model.recipientModel)
|
||||||
|
remove.setOnClickListener { model.onRemoveClick(model.recipientModel.recipient.id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ object RecipientPreference {
|
||||||
class Model(
|
class Model(
|
||||||
val recipient: Recipient,
|
val recipient: Recipient,
|
||||||
val isAdmin: Boolean = false,
|
val isAdmin: Boolean = false,
|
||||||
val onClick: () -> Unit
|
val onClick: (() -> Unit)? = null
|
||||||
) : PreferenceModel<Model>() {
|
) : PreferenceModel<Model>() {
|
||||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
return recipient.id == newItem.recipient.id
|
return recipient.id == newItem.recipient.id
|
||||||
|
@ -36,15 +36,19 @@ object RecipientPreference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.recipient_avatar)
|
private val avatar: AvatarImageView = itemView.findViewById(R.id.recipient_avatar)
|
||||||
private val name: TextView = itemView.findViewById(R.id.recipient_name)
|
private val name: TextView = itemView.findViewById(R.id.recipient_name)
|
||||||
private val about: TextView = itemView.findViewById(R.id.recipient_about)
|
private val about: TextView? = itemView.findViewById(R.id.recipient_about)
|
||||||
private val admin: View = itemView.findViewById(R.id.admin)
|
private val admin: View? = itemView.findViewById(R.id.admin)
|
||||||
private val badge: BadgeImageView = itemView.findViewById(R.id.recipient_badge)
|
private val badge: BadgeImageView = itemView.findViewById(R.id.recipient_badge)
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
itemView.setOnClickListener { model.onClick() }
|
if (model.onClick != null) {
|
||||||
|
itemView.setOnClickListener { model.onClick.invoke() }
|
||||||
|
} else {
|
||||||
|
itemView.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
avatar.setRecipient(model.recipient)
|
avatar.setRecipient(model.recipient)
|
||||||
badge.setBadgeFromRecipient(model.recipient)
|
badge.setBadgeFromRecipient(model.recipient)
|
||||||
|
@ -56,13 +60,13 @@ object RecipientPreference {
|
||||||
|
|
||||||
val aboutText = model.recipient.combinedAboutAndEmoji
|
val aboutText = model.recipient.combinedAboutAndEmoji
|
||||||
if (aboutText.isNullOrEmpty()) {
|
if (aboutText.isNullOrEmpty()) {
|
||||||
about.visibility = View.GONE
|
about?.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
about.text = model.recipient.combinedAboutAndEmoji
|
about?.text = model.recipient.combinedAboutAndEmoji
|
||||||
about.visibility = View.VISIBLE
|
about?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
admin.visible = model.isAdmin
|
admin?.visible = model.isAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ import com.annimon.stream.Collectors;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
|
@ -127,6 +128,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
@ -155,6 +157,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.thoughtcrime.securesms.util.TopToastPopup;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||||
|
@ -162,6 +165,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
||||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -367,6 +371,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
ViewUtil.setTopMargin(scrollDateHeader, topMargin + ViewUtil.dpToPx(8));
|
ViewUtil.setTopMargin(scrollDateHeader, topMargin + ViewUtil.dpToPx(8));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
conversationViewModel.getActiveNotificationProfile().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1046,6 +1052,16 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateNotificationProfileStatus(@NonNull Optional<NotificationProfile> activeProfile) {
|
||||||
|
if (activeProfile.isPresent() && activeProfile.get().getId() != SignalStore.notificationProfileValues().getLastProfilePopup()) {
|
||||||
|
requireView().postDelayed(() -> {
|
||||||
|
SignalStore.notificationProfileValues().setLastProfilePopup(activeProfile.get().getId());
|
||||||
|
SignalStore.notificationProfileValues().setLastProfilePopupTime(System.currentTimeMillis());
|
||||||
|
TopToastPopup.show(((ViewGroup) requireView()), R.drawable.ic_moon_16, getString(R.string.ConversationFragment__s_on, activeProfile.get().getName()));
|
||||||
|
}, 500L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isAtBottom() {
|
private boolean isAtBottom() {
|
||||||
if (list.getChildCount() == 0) return true;
|
if (list.getChildCount() == 0) return true;
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.app.Application;
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.Transformations;
|
import androidx.lifecycle.Transformations;
|
||||||
|
@ -22,6 +23,7 @@ import org.signal.paging.PagedData;
|
||||||
import org.signal.paging.PagingConfig;
|
import org.signal.paging.PagingConfig;
|
||||||
import org.signal.paging.PagingController;
|
import org.signal.paging.PagingController;
|
||||||
import org.signal.paging.ProxyPagingController;
|
import org.signal.paging.ProxyPagingController;
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.NameColor;
|
import org.thoughtcrime.securesms.conversation.colors.NameColor;
|
||||||
|
@ -33,6 +35,8 @@ import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
||||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
@ -53,6 +57,10 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||||
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
|
||||||
public class ConversationViewModel extends ViewModel {
|
public class ConversationViewModel extends ViewModel {
|
||||||
|
|
||||||
|
@ -81,6 +89,7 @@ public class ConversationViewModel extends ViewModel {
|
||||||
private final LiveData<Integer> conversationTopMargin;
|
private final LiveData<Integer> conversationTopMargin;
|
||||||
private final Store<ThreadAnimationState> threadAnimationStateStore;
|
private final Store<ThreadAnimationState> threadAnimationStateStore;
|
||||||
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
|
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
|
||||||
|
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||||
|
|
||||||
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
|
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
|
||||||
|
|
||||||
|
@ -88,23 +97,24 @@ public class ConversationViewModel extends ViewModel {
|
||||||
private int jumpToPosition;
|
private int jumpToPosition;
|
||||||
|
|
||||||
private ConversationViewModel() {
|
private ConversationViewModel() {
|
||||||
this.context = ApplicationDependencies.getApplication();
|
this.context = ApplicationDependencies.getApplication();
|
||||||
this.mediaRepository = new MediaRepository();
|
this.mediaRepository = new MediaRepository();
|
||||||
this.conversationRepository = new ConversationRepository();
|
this.conversationRepository = new ConversationRepository();
|
||||||
this.recentMedia = new MutableLiveData<>();
|
this.recentMedia = new MutableLiveData<>();
|
||||||
this.threadId = new MutableLiveData<>();
|
this.threadId = new MutableLiveData<>();
|
||||||
this.showScrollButtons = new MutableLiveData<>(false);
|
this.showScrollButtons = new MutableLiveData<>(false);
|
||||||
this.hasUnreadMentions = new MutableLiveData<>(false);
|
this.hasUnreadMentions = new MutableLiveData<>(false);
|
||||||
this.recipientId = new MutableLiveData<>();
|
this.recipientId = new MutableLiveData<>();
|
||||||
this.events = new SingleLiveEvent<>();
|
this.events = new SingleLiveEvent<>();
|
||||||
this.pagingController = new ProxyPagingController<>();
|
this.pagingController = new ProxyPagingController<>();
|
||||||
this.conversationObserver = pagingController::onDataInvalidated;
|
this.conversationObserver = pagingController::onDataInvalidated;
|
||||||
this.messageUpdateObserver = pagingController::onDataItemChanged;
|
this.messageUpdateObserver = pagingController::onDataItemChanged;
|
||||||
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
|
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
|
||||||
this.toolbarBottom = new MutableLiveData<>();
|
this.toolbarBottom = new MutableLiveData<>();
|
||||||
this.inlinePlayerHeight = new MutableLiveData<>();
|
this.inlinePlayerHeight = new MutableLiveData<>();
|
||||||
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
|
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
|
||||||
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
|
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
|
||||||
|
this.notificationProfilesRepository = new NotificationProfilesRepository();
|
||||||
|
|
||||||
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
|
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
|
||||||
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
|
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
|
||||||
|
@ -324,6 +334,13 @@ public class ConversationViewModel extends ViewModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<Optional<NotificationProfile>> getActiveNotificationProfile() {
|
||||||
|
final Observable<Optional<NotificationProfile>> activeProfile = Observable.combineLatest(Observable.interval(0, 30, TimeUnit.SECONDS), notificationProfilesRepository.getProfiles(), (interval, profiles) -> profiles)
|
||||||
|
.map(profiles -> Optional.fromNullable(NotificationProfiles.getActiveProfile(profiles)));
|
||||||
|
|
||||||
|
return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST));
|
||||||
|
}
|
||||||
|
|
||||||
long getLastSeen() {
|
long getLastSeen() {
|
||||||
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0;
|
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,12 +54,15 @@ import androidx.annotation.WorkerThread;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.appcompat.view.ActionMode;
|
import androidx.appcompat.view.ActionMode;
|
||||||
|
import androidx.appcompat.widget.ActionMenuView;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.appcompat.widget.TooltipCompat;
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
@ -86,6 +89,7 @@ import org.thoughtcrime.securesms.badges.models.Badge;
|
||||||
import org.thoughtcrime.securesms.badges.self.expired.ExpiredBadgeBottomSheetDialogFragment;
|
import org.thoughtcrime.securesms.badges.self.expired.ExpiredBadgeBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.components.RatingManager;
|
import org.thoughtcrime.securesms.components.RatingManager;
|
||||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||||
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
import org.thoughtcrime.securesms.components.UnreadPaymentsView;
|
import org.thoughtcrime.securesms.components.UnreadPaymentsView;
|
||||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||||
|
@ -100,6 +104,7 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
||||||
|
@ -122,6 +127,8 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder;
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
||||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
||||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsFragmentArgs;
|
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsFragmentArgs;
|
||||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsParcelable;
|
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsParcelable;
|
||||||
|
@ -137,6 +144,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||||
import org.thoughtcrime.securesms.util.AppStartup;
|
import org.thoughtcrime.securesms.util.AppStartup;
|
||||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||||
|
@ -145,6 +153,7 @@ import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.thoughtcrime.securesms.util.TopToastPopup;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||||
|
@ -169,6 +178,7 @@ import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static android.app.Activity.RESULT_OK;
|
import static android.app.Activity.RESULT_OK;
|
||||||
|
import static org.thoughtcrime.securesms.components.TooltipPopup.POSITION_BELOW;
|
||||||
|
|
||||||
|
|
||||||
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
|
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
|
||||||
|
@ -184,33 +194,35 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
|
|
||||||
private static final int MAXIMUM_PINNED_CONVERSATIONS = 4;
|
private static final int MAXIMUM_PINNED_CONVERSATIONS = 4;
|
||||||
|
|
||||||
private ActionMode actionMode;
|
private ActionMode actionMode;
|
||||||
private ConstraintLayout constraintLayout;
|
private ConstraintLayout constraintLayout;
|
||||||
private RecyclerView list;
|
private RecyclerView list;
|
||||||
private Stub<ReminderView> reminderView;
|
private Stub<ReminderView> reminderView;
|
||||||
private Stub<UnreadPaymentsView> paymentNotificationView;
|
private Stub<UnreadPaymentsView> paymentNotificationView;
|
||||||
private Stub<ViewGroup> emptyState;
|
private Stub<ViewGroup> emptyState;
|
||||||
private TextView searchEmptyState;
|
private TextView searchEmptyState;
|
||||||
private PulsingFloatingActionButton fab;
|
private PulsingFloatingActionButton fab;
|
||||||
private PulsingFloatingActionButton cameraFab;
|
private PulsingFloatingActionButton cameraFab;
|
||||||
private Stub<SearchToolbar> searchToolbar;
|
private Stub<SearchToolbar> searchToolbar;
|
||||||
private ImageView proxyStatus;
|
private ImageView notificationProfileStatus;
|
||||||
private ImageView searchAction;
|
private ImageView proxyStatus;
|
||||||
private View toolbarShadow;
|
private ImageView searchAction;
|
||||||
private View unreadPaymentsDot;
|
private View toolbarShadow;
|
||||||
private ConversationListViewModel viewModel;
|
private View unreadPaymentsDot;
|
||||||
private RecyclerView.Adapter activeAdapter;
|
private ConversationListViewModel viewModel;
|
||||||
private ConversationListAdapter defaultAdapter;
|
private RecyclerView.Adapter activeAdapter;
|
||||||
private ConversationListSearchAdapter searchAdapter;
|
private ConversationListAdapter defaultAdapter;
|
||||||
private StickyHeaderDecoration searchAdapterDecoration;
|
private ConversationListSearchAdapter searchAdapter;
|
||||||
private Stub<ViewGroup> megaphoneContainer;
|
private StickyHeaderDecoration searchAdapterDecoration;
|
||||||
private SnapToTopDataObserver snapToTopDataObserver;
|
private Stub<ViewGroup> megaphoneContainer;
|
||||||
private Drawable archiveDrawable;
|
private SnapToTopDataObserver snapToTopDataObserver;
|
||||||
private AppForegroundObserver.Listener appForegroundObserver;
|
private Drawable archiveDrawable;
|
||||||
private VoiceNoteMediaControllerOwner mediaControllerOwner;
|
private AppForegroundObserver.Listener appForegroundObserver;
|
||||||
private Stub<FrameLayout> voiceNotePlayerViewStub;
|
private VoiceNoteMediaControllerOwner mediaControllerOwner;
|
||||||
private VoiceNotePlayerView voiceNotePlayerView;
|
private Stub<FrameLayout> voiceNotePlayerViewStub;
|
||||||
private SignalBottomActionBar bottomActionBar;
|
private VoiceNotePlayerView voiceNotePlayerView;
|
||||||
|
private SignalBottomActionBar bottomActionBar;
|
||||||
|
private TopToastPopup previousTopToastPopup;
|
||||||
|
|
||||||
protected ConversationListArchiveItemDecoration archiveDecoration;
|
protected ConversationListArchiveItemDecoration archiveDecoration;
|
||||||
protected ConversationListItemAnimator itemAnimator;
|
protected ConversationListItemAnimator itemAnimator;
|
||||||
|
@ -245,27 +257,29 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
constraintLayout = view.findViewById(R.id.constraint_layout);
|
constraintLayout = view.findViewById(R.id.constraint_layout);
|
||||||
list = view.findViewById(R.id.list);
|
list = view.findViewById(R.id.list);
|
||||||
fab = view.findViewById(R.id.fab);
|
fab = view.findViewById(R.id.fab);
|
||||||
cameraFab = view.findViewById(R.id.camera_fab);
|
cameraFab = view.findViewById(R.id.camera_fab);
|
||||||
searchEmptyState = view.findViewById(R.id.search_no_results);
|
searchEmptyState = view.findViewById(R.id.search_no_results);
|
||||||
searchAction = view.findViewById(R.id.search_action);
|
searchAction = view.findViewById(R.id.search_action);
|
||||||
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
|
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
|
||||||
proxyStatus = view.findViewById(R.id.conversation_list_proxy_status);
|
notificationProfileStatus = view.findViewById(R.id.conversation_list_notification_profile_status);
|
||||||
unreadPaymentsDot = view.findViewById(R.id.unread_payments_indicator);
|
proxyStatus = view.findViewById(R.id.conversation_list_proxy_status);
|
||||||
bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar);
|
unreadPaymentsDot = view.findViewById(R.id.unread_payments_indicator);
|
||||||
reminderView = new Stub<>(view.findViewById(R.id.reminder));
|
bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar);
|
||||||
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
|
reminderView = new Stub<>(view.findViewById(R.id.reminder));
|
||||||
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
|
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
|
||||||
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
|
||||||
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
||||||
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
||||||
|
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||||
|
|
||||||
Toolbar toolbar = getToolbar(view);
|
Toolbar toolbar = getToolbar(view);
|
||||||
toolbar.setVisibility(View.VISIBLE);
|
toolbar.setVisibility(View.VISIBLE);
|
||||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
notificationProfileStatus.setOnClickListener(v -> handleNotificationProfile());
|
||||||
proxyStatus.setOnClickListener(v -> onProxyStatusClicked());
|
proxyStatus.setOnClickListener(v -> onProxyStatusClicked());
|
||||||
|
|
||||||
fab.show();
|
fab.show();
|
||||||
|
@ -307,6 +321,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
|
TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
previousTopToastPopup = null;
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
@ -389,12 +409,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
super.onOptionsItemSelected(item);
|
super.onOptionsItemSelected(item);
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.menu_new_group: handleCreateGroup(); return true;
|
case R.id.menu_new_group: handleCreateGroup(); return true;
|
||||||
case R.id.menu_settings: handleDisplaySettings(); return true;
|
case R.id.menu_settings: handleDisplaySettings(); return true;
|
||||||
case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
|
case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
|
||||||
case R.id.menu_mark_all_read: handleMarkAllRead(); return true;
|
case R.id.menu_mark_all_read: handleMarkAllRead(); return true;
|
||||||
case R.id.menu_invite: handleInvite(); return true;
|
case R.id.menu_invite: handleInvite(); return true;
|
||||||
case R.id.menu_insights: handleInsights(); return true;
|
case R.id.menu_insights: handleInsights(); return true;
|
||||||
|
case R.id.menu_notification_profile: handleNotificationProfile(); return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -652,12 +673,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeViewModel() {
|
private void initializeViewModel() {
|
||||||
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
|
viewModel = new ViewModelProvider(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
|
||||||
|
|
||||||
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
||||||
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
||||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onConversationListChanged);
|
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onConversationListChanged);
|
||||||
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
||||||
|
viewModel.getNotificationProfiles().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus);
|
||||||
viewModel.getPipeState().observe(getViewLifecycleOwner(), this::updateProxyStatus);
|
viewModel.getPipeState().observe(getViewLifecycleOwner(), this::updateProxyStatus);
|
||||||
|
|
||||||
appForegroundObserver = new AppForegroundObserver.Listener() {
|
appForegroundObserver = new AppForegroundObserver.Listener() {
|
||||||
|
@ -844,6 +866,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
getNavigator().goToInsights();
|
getNavigator().goToInsights();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleNotificationProfile() {
|
||||||
|
NotificationProfileSelectionFragment.show(getParentFragmentManager());
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private void handleArchive(@NonNull Collection<Long> ids, boolean showProgress) {
|
private void handleArchive(@NonNull Collection<Long> ids, boolean showProgress) {
|
||||||
Set<Long> selectedConversations = new HashSet<>(ids);
|
Set<Long> selectedConversations = new HashSet<>(ids);
|
||||||
|
@ -1042,6 +1068,67 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateNotificationProfileStatus(@NonNull List<NotificationProfile> notificationProfiles) {
|
||||||
|
if (notificationProfiles.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SignalStore.notificationProfileValues().getHasSeenTooltip()) {
|
||||||
|
View target = findOverflowMenuButton(getToolbar(requireView()));
|
||||||
|
if (target != null) {
|
||||||
|
TooltipPopup.forTarget(target)
|
||||||
|
.setText(R.string.ConversationListFragment__turn_your_notification_profile_on_or_off_here)
|
||||||
|
.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.signal_button_primary))
|
||||||
|
.setTextColor(ContextCompat.getColor(requireContext(), R.color.signal_button_primary_text))
|
||||||
|
.setOnDismissListener(() -> SignalStore.notificationProfileValues().setHasSeenTooltip(true))
|
||||||
|
.show(POSITION_BELOW);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Unable to find overflow menu to show Notification Profile tooltip");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationProfile activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles);
|
||||||
|
|
||||||
|
if (activeProfile != null) {
|
||||||
|
if (activeProfile.getId() != SignalStore.notificationProfileValues().getLastProfilePopup()) {
|
||||||
|
requireView().postDelayed(() -> {
|
||||||
|
SignalStore.notificationProfileValues().setLastProfilePopup(activeProfile.getId());
|
||||||
|
SignalStore.notificationProfileValues().setLastProfilePopupTime(System.currentTimeMillis());
|
||||||
|
|
||||||
|
if (previousTopToastPopup != null && previousTopToastPopup.isShowing()) {
|
||||||
|
previousTopToastPopup.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewGroup view = ((ViewGroup) requireView());
|
||||||
|
Fragment fragment = getParentFragmentManager().findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||||
|
if (fragment != null && fragment.isAdded() && fragment.getView() != null) {
|
||||||
|
view = ((ViewGroup) fragment.requireView());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
previousTopToastPopup = TopToastPopup.show(view, R.drawable.ic_moon_16, getString(R.string.ConversationListFragment__s_on, activeProfile.getName()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Unable to show toast popup", e);
|
||||||
|
}
|
||||||
|
}, 500L);
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationProfileStatus.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
notificationProfileStatus.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable View findOverflowMenuButton(@NonNull Toolbar viewGroup) {
|
||||||
|
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||||
|
View v = viewGroup.getChildAt(i);
|
||||||
|
if (v instanceof ActionMenuView) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void updateProxyStatus(@NonNull WebSocketConnectionState state) {
|
private void updateProxyStatus(@NonNull WebSocketConnectionState state) {
|
||||||
if (SignalStore.proxy().isProxyEnabled()) {
|
if (SignalStore.proxy().isProxyEnabled()) {
|
||||||
proxyStatus.setVisibility(View.VISIBLE);
|
proxyStatus.setVisibility(View.VISIBLE);
|
||||||
|
@ -1250,7 +1337,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
|
|
||||||
items.add(new ActionItem(R.drawable.ic_select_24, getString(R.string.ConversationListFragment_select_all), viewModel::onSelectAllClick));
|
items.add(new ActionItem(R.drawable.ic_select_24, getString(R.string.ConversationListFragment_select_all), viewModel::onSelectAllClick));
|
||||||
|
|
||||||
bottomActionBar.setItems(items);
|
// bottomActionBar.setItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Toolbar getToolbar(@NonNull View rootView) {
|
protected Toolbar getToolbar(@NonNull View rootView) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.signal.core.util.logging.Log;
|
||||||
import org.signal.paging.PagedData;
|
import org.signal.paging.PagedData;
|
||||||
import org.signal.paging.PagingConfig;
|
import org.signal.paging.PagingConfig;
|
||||||
import org.signal.paging.PagingController;
|
import org.signal.paging.PagingController;
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||||
|
@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||||
import org.thoughtcrime.securesms.payments.UnreadPaymentsRepository;
|
import org.thoughtcrime.securesms.payments.UnreadPaymentsRepository;
|
||||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||||
import org.thoughtcrime.securesms.search.SearchResult;
|
import org.thoughtcrime.securesms.search.SearchResult;
|
||||||
|
@ -39,9 +41,11 @@ import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||||
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
@ -52,50 +56,52 @@ class ConversationListViewModel extends ViewModel {
|
||||||
|
|
||||||
private static boolean coldStart = true;
|
private static boolean coldStart = true;
|
||||||
|
|
||||||
private final MutableLiveData<Megaphone> megaphone;
|
private final MutableLiveData<Megaphone> megaphone;
|
||||||
private final MutableLiveData<SearchResult> searchResult;
|
private final MutableLiveData<SearchResult> searchResult;
|
||||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||||
private final Set<Conversation> internalSelection;
|
private final Set<Conversation> internalSelection;
|
||||||
private final ConversationListDataSource conversationListDataSource;
|
private final ConversationListDataSource conversationListDataSource;
|
||||||
private final PagedData<Long, Conversation> pagedData;
|
private final PagedData<Long, Conversation> pagedData;
|
||||||
private final LiveData<Boolean> hasNoConversations;
|
private final LiveData<Boolean> hasNoConversations;
|
||||||
private final SearchRepository searchRepository;
|
private final SearchRepository searchRepository;
|
||||||
private final MegaphoneRepository megaphoneRepository;
|
private final MegaphoneRepository megaphoneRepository;
|
||||||
private final Debouncer messageSearchDebouncer;
|
private final Debouncer messageSearchDebouncer;
|
||||||
private final Debouncer contactSearchDebouncer;
|
private final Debouncer contactSearchDebouncer;
|
||||||
private final ThrottledDebouncer updateDebouncer;
|
private final ThrottledDebouncer updateDebouncer;
|
||||||
private final DatabaseObserver.Observer observer;
|
private final DatabaseObserver.Observer observer;
|
||||||
private final Invalidator invalidator;
|
private final Invalidator invalidator;
|
||||||
private final CompositeDisposable disposables;
|
private final CompositeDisposable disposables;
|
||||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||||
|
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||||
|
|
||||||
private String activeQuery;
|
private String activeQuery;
|
||||||
private SearchResult activeSearchResult;
|
private SearchResult activeSearchResult;
|
||||||
private int pinnedCount;
|
private int pinnedCount;
|
||||||
|
|
||||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||||
this.megaphone = new MutableLiveData<>();
|
this.megaphone = new MutableLiveData<>();
|
||||||
this.searchResult = new MutableLiveData<>();
|
this.searchResult = new MutableLiveData<>();
|
||||||
this.internalSelection = new HashSet<>();
|
this.internalSelection = new HashSet<>();
|
||||||
this.selectedConversations = new MutableLiveData<>(new ConversationSet());
|
this.selectedConversations = new MutableLiveData<>(new ConversationSet());
|
||||||
this.searchRepository = searchRepository;
|
this.searchRepository = searchRepository;
|
||||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||||
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
this.unreadPaymentsRepository = new UnreadPaymentsRepository();
|
||||||
this.messageSearchDebouncer = new Debouncer(500);
|
this.notificationProfilesRepository = new NotificationProfilesRepository();
|
||||||
this.contactSearchDebouncer = new Debouncer(100);
|
this.messageSearchDebouncer = new Debouncer(500);
|
||||||
this.updateDebouncer = new ThrottledDebouncer(500);
|
this.contactSearchDebouncer = new Debouncer(100);
|
||||||
this.activeSearchResult = SearchResult.EMPTY;
|
this.updateDebouncer = new ThrottledDebouncer(500);
|
||||||
this.invalidator = new Invalidator();
|
this.activeSearchResult = SearchResult.EMPTY;
|
||||||
this.disposables = new CompositeDisposable();
|
this.invalidator = new Invalidator();
|
||||||
this.conversationListDataSource = ConversationListDataSource.create(application, isArchived);
|
this.disposables = new CompositeDisposable();
|
||||||
this.pagedData = PagedData.create(conversationListDataSource,
|
this.conversationListDataSource = ConversationListDataSource.create(application, isArchived);
|
||||||
new PagingConfig.Builder()
|
this.pagedData = PagedData.create(conversationListDataSource,
|
||||||
.setPageSize(15)
|
new PagingConfig.Builder()
|
||||||
.setBufferPages(2)
|
.setPageSize(15)
|
||||||
.build());
|
.setBufferPages(2)
|
||||||
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
.build());
|
||||||
this.observer = () -> {
|
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
||||||
|
this.observer = () -> {
|
||||||
updateDebouncer.publish(() -> {
|
updateDebouncer.publish(() -> {
|
||||||
if (!TextUtils.isEmpty(activeQuery)) {
|
if (!TextUtils.isEmpty(activeQuery)) {
|
||||||
onSearchQueryUpdated(activeQuery);
|
onSearchQueryUpdated(activeQuery);
|
||||||
|
@ -137,6 +143,12 @@ class ConversationListViewModel extends ViewModel {
|
||||||
return pagedData.getController();
|
return pagedData.getController();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<List<NotificationProfile>> getNotificationProfiles() {
|
||||||
|
final Observable<List<NotificationProfile>> activeProfile = Observable.combineLatest(Observable.interval(0, 30, TimeUnit.SECONDS), notificationProfilesRepository.getProfiles(), (interval, profiles) -> profiles);
|
||||||
|
|
||||||
|
return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST));
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull LiveData<WebSocketConnectionState> getPipeState() {
|
@NonNull LiveData<WebSocketConnectionState> getPipeState() {
|
||||||
return LiveDataReactiveStreams.fromPublisher(ApplicationDependencies.getSignalWebSocket().getWebSocketState().toFlowable(BackpressureStrategy.LATEST));
|
return LiveDataReactiveStreams.fromPublisher(ApplicationDependencies.getSignalWebSocket().getWebSocketState().toFlowable(BackpressureStrategy.LATEST));
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,3 +55,5 @@ fun Cursor.optionalBlob(column: String): Optional<ByteArray> {
|
||||||
fun Cursor.isNull(column: String): Boolean {
|
fun Cursor.isNull(column: String): Boolean {
|
||||||
return CursorUtil.isNull(this, column)
|
return CursorUtil.isNull(this, column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Boolean.toInt(): Int = if (this) 1 else 0
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.app.Application;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||||
|
@ -20,7 +21,7 @@ import java.util.concurrent.Executor;
|
||||||
*
|
*
|
||||||
* A replacement for the observer system in {@link Database}. We should move to this over time.
|
* A replacement for the observer system in {@link Database}. We should move to this over time.
|
||||||
*/
|
*/
|
||||||
public final class DatabaseObserver {
|
public class DatabaseObserver {
|
||||||
|
|
||||||
private final Application application;
|
private final Application application;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
|
@ -36,6 +37,7 @@ public final class DatabaseObserver {
|
||||||
private final Set<Observer> attachmentObservers;
|
private final Set<Observer> attachmentObservers;
|
||||||
private final Set<MessageObserver> messageUpdateObservers;
|
private final Set<MessageObserver> messageUpdateObservers;
|
||||||
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
|
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
|
||||||
|
private final Set<Observer> notificationProfileObservers;
|
||||||
|
|
||||||
public DatabaseObserver(Application application) {
|
public DatabaseObserver(Application application) {
|
||||||
this.application = application;
|
this.application = application;
|
||||||
|
@ -51,6 +53,7 @@ public final class DatabaseObserver {
|
||||||
this.attachmentObservers = new HashSet<>();
|
this.attachmentObservers = new HashSet<>();
|
||||||
this.messageUpdateObservers = new HashSet<>();
|
this.messageUpdateObservers = new HashSet<>();
|
||||||
this.messageInsertObservers = new HashMap<>();
|
this.messageInsertObservers = new HashMap<>();
|
||||||
|
this.notificationProfileObservers = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerConversationListObserver(@NonNull Observer listener) {
|
public void registerConversationListObserver(@NonNull Observer listener) {
|
||||||
|
@ -119,6 +122,12 @@ public final class DatabaseObserver {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void registerNotificationProfileObserver(@NotNull Observer listener) {
|
||||||
|
executor.execute(() -> {
|
||||||
|
notificationProfileObservers.add(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void unregisterObserver(@NonNull Observer listener) {
|
public void unregisterObserver(@NonNull Observer listener) {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
conversationListObservers.remove(listener);
|
conversationListObservers.remove(listener);
|
||||||
|
@ -129,6 +138,7 @@ public final class DatabaseObserver {
|
||||||
stickerObservers.remove(listener);
|
stickerObservers.remove(listener);
|
||||||
stickerPackObservers.remove(listener);
|
stickerPackObservers.remove(listener);
|
||||||
attachmentObservers.remove(listener);
|
attachmentObservers.remove(listener);
|
||||||
|
notificationProfileObservers.remove(listener);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +241,12 @@ public final class DatabaseObserver {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void notifyNotificationProfileObservers() {
|
||||||
|
executor.execute(() -> {
|
||||||
|
notifySet(notificationProfileObservers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private <K, V> void registerMapped(@NonNull Map<K, Set<V>> map, @NonNull K key, @NonNull V listener) {
|
private <K, V> void registerMapped(@NonNull Map<K, Set<V>> map, @NonNull K key, @NonNull V listener) {
|
||||||
Set<V> listeners = map.get(key);
|
Set<V> listeners = map.get(key);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,388 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import net.zetetic.database.sqlcipher.SQLiteConstraintException
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.SqlUtil
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
|
||||||
|
*/
|
||||||
|
class NotificationProfileDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATE_TABLE: Array<String> = arrayOf(NotificationProfileTable.CREATE_TABLE, NotificationProfileScheduleTable.CREATE_TABLE, NotificationProfileAllowedMembersTable.CREATE_TABLE)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATE_INDEXES: Array<String> = arrayOf(NotificationProfileScheduleTable.CREATE_INDEX, NotificationProfileAllowedMembersTable.CREATE_INDEX)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object NotificationProfileTable {
|
||||||
|
const val TABLE_NAME = "notification_profile"
|
||||||
|
|
||||||
|
const val ID = "_id"
|
||||||
|
const val NAME = "name"
|
||||||
|
const val EMOJI = "emoji"
|
||||||
|
const val COLOR = "color"
|
||||||
|
const val CREATED_AT = "created_at"
|
||||||
|
const val ALLOW_ALL_CALLS = "allow_all_calls"
|
||||||
|
const val ALLOW_ALL_MENTIONS = "allow_all_mentions"
|
||||||
|
|
||||||
|
val CREATE_TABLE = """
|
||||||
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
$NAME TEXT NOT NULL UNIQUE,
|
||||||
|
$EMOJI TEXT NOT NULL,
|
||||||
|
$COLOR TEXT NOT NULL,
|
||||||
|
$CREATED_AT INTEGER NOT NULL,
|
||||||
|
$ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0,
|
||||||
|
$ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private object NotificationProfileScheduleTable {
|
||||||
|
const val TABLE_NAME = "notification_profile_schedule"
|
||||||
|
|
||||||
|
const val ID = "_id"
|
||||||
|
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
|
||||||
|
const val ENABLED = "enabled"
|
||||||
|
const val START = "start"
|
||||||
|
const val END = "end"
|
||||||
|
const val DAYS_ENABLED = "days_enabled"
|
||||||
|
|
||||||
|
val DEFAULT_DAYS = listOf(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY).serialize()
|
||||||
|
|
||||||
|
val CREATE_TABLE = """
|
||||||
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
$NOTIFICATION_PROFILE_ID INTEGER NOT NULL REFERENCES ${NotificationProfileTable.TABLE_NAME} (${NotificationProfileTable.ID}) ON DELETE CASCADE,
|
||||||
|
$ENABLED INTEGER NOT NULL DEFAULT 0,
|
||||||
|
$START INTEGER NOT NULL,
|
||||||
|
$END INTEGER NOT NULL,
|
||||||
|
$DAYS_ENABLED TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
const val CREATE_INDEX = "CREATE INDEX notification_profile_schedule_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private object NotificationProfileAllowedMembersTable {
|
||||||
|
const val TABLE_NAME = "notification_profile_allowed_members"
|
||||||
|
|
||||||
|
const val ID = "_id"
|
||||||
|
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
|
||||||
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
|
|
||||||
|
val CREATE_TABLE = """
|
||||||
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
$NOTIFICATION_PROFILE_ID INTEGER NOT NULL REFERENCES ${NotificationProfileTable.TABLE_NAME} (${NotificationProfileTable.ID}) ON DELETE CASCADE,
|
||||||
|
$RECIPIENT_ID INTEGER NOT NULL,
|
||||||
|
UNIQUE($NOTIFICATION_PROFILE_ID, $RECIPIENT_ID) ON CONFLICT REPLACE
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
const val CREATE_INDEX = "CREATE INDEX notification_profile_allowed_members_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createProfile(name: String, emoji: String, color: AvatarColor, createdAt: Long): NotificationProfileChangeResult {
|
||||||
|
val db = writableDatabase
|
||||||
|
|
||||||
|
db.beginTransaction()
|
||||||
|
try {
|
||||||
|
val profileValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileTable.NAME, name)
|
||||||
|
put(NotificationProfileTable.EMOJI, emoji)
|
||||||
|
put(NotificationProfileTable.COLOR, color.serialize())
|
||||||
|
put(NotificationProfileTable.CREATED_AT, createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues)
|
||||||
|
if (profileId < 0) {
|
||||||
|
return NotificationProfileChangeResult.DuplicateName
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheduleValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID, profileId)
|
||||||
|
put(NotificationProfileScheduleTable.START, 900)
|
||||||
|
put(NotificationProfileScheduleTable.END, 1700)
|
||||||
|
put(NotificationProfileScheduleTable.DAYS_ENABLED, NotificationProfileScheduleTable.DEFAULT_DAYS)
|
||||||
|
}
|
||||||
|
db.insert(NotificationProfileScheduleTable.TABLE_NAME, null, scheduleValues)
|
||||||
|
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
|
||||||
|
return NotificationProfileChangeResult.Success(
|
||||||
|
NotificationProfile(
|
||||||
|
id = profileId,
|
||||||
|
name = name,
|
||||||
|
emoji = emoji,
|
||||||
|
createdAt = createdAt,
|
||||||
|
schedule = getProfileSchedule(profileId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
db.endTransaction()
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProfile(profileId: Long, name: String, emoji: String): NotificationProfileChangeResult {
|
||||||
|
val profileValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileTable.NAME, name)
|
||||||
|
put(NotificationProfileTable.EMOJI, emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profileId), profileValues)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val count = writableDatabase.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
|
||||||
|
if (count > 0) {
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationProfileChangeResult.Success(getProfile(profileId)!!)
|
||||||
|
} catch (e: SQLiteConstraintException) {
|
||||||
|
NotificationProfileChangeResult.DuplicateName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProfile(profile: NotificationProfile): NotificationProfileChangeResult {
|
||||||
|
val db = writableDatabase
|
||||||
|
|
||||||
|
db.beginTransaction()
|
||||||
|
try {
|
||||||
|
val profileValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileTable.NAME, profile.name)
|
||||||
|
put(NotificationProfileTable.EMOJI, profile.emoji)
|
||||||
|
put(NotificationProfileTable.ALLOW_ALL_CALLS, profile.allowAllCalls.toInt())
|
||||||
|
put(NotificationProfileTable.ALLOW_ALL_MENTIONS, profile.allowAllMentions.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profile.id), profileValues)
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
|
||||||
|
} catch (e: SQLiteConstraintException) {
|
||||||
|
return NotificationProfileChangeResult.DuplicateName
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSchedule(profile.schedule, true)
|
||||||
|
|
||||||
|
db.delete(NotificationProfileAllowedMembersTable.TABLE_NAME, "${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ?", SqlUtil.buildArgs(profile.id))
|
||||||
|
|
||||||
|
profile.allowedMembers.forEach { recipientId ->
|
||||||
|
val allowedMembersValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID, profile.id)
|
||||||
|
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, recipientId.serialize())
|
||||||
|
}
|
||||||
|
db.insert(NotificationProfileAllowedMembersTable.TABLE_NAME, null, allowedMembersValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
|
||||||
|
return NotificationProfileChangeResult.Success(getProfile(profile.id)!!)
|
||||||
|
} finally {
|
||||||
|
db.endTransaction()
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSchedule(schedule: NotificationProfileSchedule, silent: Boolean = false) {
|
||||||
|
val scheduleValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileScheduleTable.ENABLED, schedule.enabled.toInt())
|
||||||
|
put(NotificationProfileScheduleTable.START, schedule.start)
|
||||||
|
put(NotificationProfileScheduleTable.END, schedule.end)
|
||||||
|
put(NotificationProfileScheduleTable.DAYS_ENABLED, schedule.daysEnabled.serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(schedule.id), scheduleValues)
|
||||||
|
writableDatabase.update(NotificationProfileScheduleTable.TABLE_NAME, scheduleValues, updateQuery.where, updateQuery.whereArgs)
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAllowedRecipients(profileId: Long, recipients: Set<RecipientId>): NotificationProfile {
|
||||||
|
val db = writableDatabase
|
||||||
|
|
||||||
|
db.beginTransaction()
|
||||||
|
try {
|
||||||
|
db.delete(NotificationProfileAllowedMembersTable.TABLE_NAME, "${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ?", SqlUtil.buildArgs(profileId))
|
||||||
|
|
||||||
|
recipients.forEach { recipientId ->
|
||||||
|
val allowedMembersValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID, profileId)
|
||||||
|
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, recipientId.serialize())
|
||||||
|
}
|
||||||
|
db.insert(NotificationProfileAllowedMembersTable.TABLE_NAME, null, allowedMembersValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
|
||||||
|
return getProfile(profileId)!!
|
||||||
|
} finally {
|
||||||
|
db.endTransaction()
|
||||||
|
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAllowedRecipient(profileId: Long, recipientId: RecipientId): NotificationProfile {
|
||||||
|
val allowedValues = ContentValues().apply {
|
||||||
|
put(NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID, profileId)
|
||||||
|
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, recipientId.serialize())
|
||||||
|
}
|
||||||
|
writableDatabase.insert(NotificationProfileAllowedMembersTable.TABLE_NAME, null, allowedValues)
|
||||||
|
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
return getProfile(profileId)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAllowedRecipient(profileId: Long, recipientId: RecipientId): NotificationProfile {
|
||||||
|
writableDatabase.delete(
|
||||||
|
NotificationProfileAllowedMembersTable.TABLE_NAME,
|
||||||
|
"${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ? AND ${NotificationProfileAllowedMembersTable.RECIPIENT_ID} = ?",
|
||||||
|
SqlUtil.buildArgs(profileId, recipientId)
|
||||||
|
)
|
||||||
|
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
return getProfile(profileId)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfiles(): List<NotificationProfile> {
|
||||||
|
val profiles: MutableList<NotificationProfile> = mutableListOf()
|
||||||
|
|
||||||
|
readableDatabase.query(NotificationProfileTable.TABLE_NAME, null, null, null, null, null, null).use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
profiles += getProfile(cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfile(profileId: Long): NotificationProfile? {
|
||||||
|
return readableDatabase.query(NotificationProfileTable.TABLE_NAME, null, ID_WHERE, SqlUtil.buildArgs(profileId), null, null, null).use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
getProfile(cursor)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteProfile(profileId: Long) {
|
||||||
|
writableDatabase.delete(NotificationProfileTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(profileId))
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
|
||||||
|
val query = "${NotificationProfileAllowedMembersTable.RECIPIENT_ID} = ?"
|
||||||
|
val args = SqlUtil.buildArgs(oldId)
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, newId.serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseHelper.signalWritableDatabase.update(NotificationProfileAllowedMembersTable.TABLE_NAME, values, query, args)
|
||||||
|
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProfile(cursor: Cursor): NotificationProfile {
|
||||||
|
val profileId: Long = cursor.requireLong(NotificationProfileTable.ID)
|
||||||
|
|
||||||
|
return NotificationProfile(
|
||||||
|
id = profileId,
|
||||||
|
name = cursor.requireString(NotificationProfileTable.NAME)!!,
|
||||||
|
emoji = cursor.requireString(NotificationProfileTable.EMOJI)!!,
|
||||||
|
color = AvatarColor.deserialize(cursor.requireString(NotificationProfileTable.COLOR)),
|
||||||
|
createdAt = cursor.requireLong(NotificationProfileTable.CREATED_AT),
|
||||||
|
allowAllCalls = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_CALLS),
|
||||||
|
allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS),
|
||||||
|
schedule = getProfileSchedule(profileId),
|
||||||
|
allowedMembers = getProfileAllowedMembers(profileId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProfileSchedule(profileId: Long): NotificationProfileSchedule {
|
||||||
|
val query = SqlUtil.buildQuery("${NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID} = ?", profileId)
|
||||||
|
|
||||||
|
return readableDatabase.query(NotificationProfileScheduleTable.TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val daysEnabledString = cursor.requireString(NotificationProfileScheduleTable.DAYS_ENABLED) ?: ""
|
||||||
|
val daysEnabled: Set<DayOfWeek> = daysEnabledString.split(",")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { it.toDayOfWeek() }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
NotificationProfileSchedule(
|
||||||
|
id = cursor.requireLong(NotificationProfileScheduleTable.ID),
|
||||||
|
enabled = cursor.requireBoolean(NotificationProfileScheduleTable.ENABLED),
|
||||||
|
start = cursor.requireInt(NotificationProfileScheduleTable.START),
|
||||||
|
end = cursor.requireInt(NotificationProfileScheduleTable.END),
|
||||||
|
daysEnabled = daysEnabled
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw AssertionError("No schedule for $profileId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProfileAllowedMembers(profileId: Long): Set<RecipientId> {
|
||||||
|
val allowed = mutableSetOf<RecipientId>()
|
||||||
|
val query = SqlUtil.buildQuery("${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ?", profileId)
|
||||||
|
|
||||||
|
readableDatabase.query(NotificationProfileAllowedMembersTable.TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
allowed += RecipientId.from(cursor.requireLong(NotificationProfileAllowedMembersTable.RECIPIENT_ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class NotificationProfileChangeResult {
|
||||||
|
data class Success(val notificationProfile: NotificationProfile) : NotificationProfileChangeResult()
|
||||||
|
object DuplicateName : NotificationProfileChangeResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Iterable<DayOfWeek>.serialize(): String {
|
||||||
|
return joinToString(separator = ",", transform = { it.serialize() })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toDayOfWeek(): DayOfWeek {
|
||||||
|
return when (this) {
|
||||||
|
"1" -> DayOfWeek.MONDAY
|
||||||
|
"2" -> DayOfWeek.TUESDAY
|
||||||
|
"3" -> DayOfWeek.WEDNESDAY
|
||||||
|
"4" -> DayOfWeek.THURSDAY
|
||||||
|
"5" -> DayOfWeek.FRIDAY
|
||||||
|
"6" -> DayOfWeek.SATURDAY
|
||||||
|
"7" -> DayOfWeek.SUNDAY
|
||||||
|
else -> throw AssertionError("Value ($this) does not map to a day")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DayOfWeek.serialize(): String {
|
||||||
|
return when (this) {
|
||||||
|
DayOfWeek.MONDAY -> "1"
|
||||||
|
DayOfWeek.TUESDAY -> "2"
|
||||||
|
DayOfWeek.WEDNESDAY -> "3"
|
||||||
|
DayOfWeek.THURSDAY -> "4"
|
||||||
|
DayOfWeek.FRIDAY -> "5"
|
||||||
|
DayOfWeek.SATURDAY -> "6"
|
||||||
|
DayOfWeek.SUNDAY -> "7"
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notificationProfiles
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||||
|
@ -2592,6 +2593,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
// Reactions
|
// Reactions
|
||||||
reactions.remapRecipient(byE164, byAci)
|
reactions.remapRecipient(byE164, byAci)
|
||||||
|
|
||||||
|
// Notification Profiles
|
||||||
|
notificationProfiles.remapRecipient(byE164, byAci)
|
||||||
|
|
||||||
// Recipient
|
// Recipient
|
||||||
Log.w(TAG, "Deleting recipient $byE164", true)
|
Log.w(TAG, "Deleting recipient $byE164", true)
|
||||||
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))
|
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))
|
||||||
|
|
|
@ -70,6 +70,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
|
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
|
||||||
val groupCallRingDatabase: GroupCallRingDatabase = GroupCallRingDatabase(context, this)
|
val groupCallRingDatabase: GroupCallRingDatabase = GroupCallRingDatabase(context, this)
|
||||||
val reactionDatabase: ReactionDatabase = ReactionDatabase(context, this)
|
val reactionDatabase: ReactionDatabase = ReactionDatabase(context, this)
|
||||||
|
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
|
||||||
|
|
||||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||||
db.enableWriteAheadLogging()
|
db.enableWriteAheadLogging()
|
||||||
|
@ -105,6 +106,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
executeStatements(db, SearchDatabase.CREATE_TABLE)
|
executeStatements(db, SearchDatabase.CREATE_TABLE)
|
||||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
||||||
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
||||||
|
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
|
||||||
|
|
||||||
executeStatements(db, RecipientDatabase.CREATE_INDEXS)
|
executeStatements(db, RecipientDatabase.CREATE_INDEXS)
|
||||||
executeStatements(db, SmsDatabase.CREATE_INDEXS)
|
executeStatements(db, SmsDatabase.CREATE_INDEXS)
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS)
|
executeStatements(db, MmsDatabase.CREATE_INDEXS)
|
||||||
|
@ -119,8 +122,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
executeStatements(db, PaymentDatabase.CREATE_INDEXES)
|
executeStatements(db, PaymentDatabase.CREATE_INDEXES)
|
||||||
executeStatements(db, MessageSendLogDatabase.CREATE_INDEXES)
|
executeStatements(db, MessageSendLogDatabase.CREATE_INDEXES)
|
||||||
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES)
|
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES)
|
||||||
|
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
|
||||||
|
|
||||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
||||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
||||||
|
|
||||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||||
val legacyHelper = ClassicOpenHelper(context)
|
val legacyHelper = ClassicOpenHelper(context)
|
||||||
val legacyDb = legacyHelper.writableDatabase
|
val legacyDb = legacyHelper.writableDatabase
|
||||||
|
@ -438,5 +444,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
@get:JvmName("unknownStorageIds")
|
@get:JvmName("unknownStorageIds")
|
||||||
val unknownStorageIds: UnknownStorageIdDatabase
|
val unknownStorageIds: UnknownStorageIdDatabase
|
||||||
get() = instance!!.storageIdDatabase
|
get() = instance!!.storageIdDatabase
|
||||||
|
|
||||||
|
@get:JvmStatic
|
||||||
|
@get:JvmName("notificationProfiles")
|
||||||
|
val notificationProfiles: NotificationProfileDatabase
|
||||||
|
get() = instance!!.notificationProfileDatabase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,8 +179,9 @@ object SignalDatabaseMigrations {
|
||||||
private const val SENDER_KEY_SHARED_TIMESTAMP = 120
|
private const val SENDER_KEY_SHARED_TIMESTAMP = 120
|
||||||
private const val REACTION_REFACTOR = 121
|
private const val REACTION_REFACTOR = 121
|
||||||
private const val PNI = 122
|
private const val PNI = 122
|
||||||
|
private const val NOTIFICATION_PROFILES = 123
|
||||||
|
|
||||||
const val DATABASE_VERSION = 122
|
const val DATABASE_VERSION = 123
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
@ -2176,6 +2177,51 @@ object SignalDatabaseMigrations {
|
||||||
db.execSQL("ALTER TABLE recipient ADD COLUMN pni TEXT DEFAULT NULL")
|
db.execSQL("ALTER TABLE recipient ADD COLUMN pni TEXT DEFAULT NULL")
|
||||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON recipient (pni)")
|
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON recipient (pni)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < NOTIFICATION_PROFILES) {
|
||||||
|
db.execSQL(
|
||||||
|
// language=sql
|
||||||
|
"""
|
||||||
|
CREATE TABLE notification_profile (
|
||||||
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
allow_all_calls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
allow_all_mentions INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
// language=sql
|
||||||
|
"""
|
||||||
|
CREATE TABLE notification_profile_schedule (
|
||||||
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
start INTEGER NOT NULL,
|
||||||
|
end INTEGER NOT NULL,
|
||||||
|
days_enabled TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
// language=sql
|
||||||
|
"""
|
||||||
|
CREATE TABLE notification_profile_allowed_members (
|
||||||
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,
|
||||||
|
recipient_id INTEGER NOT NULL,
|
||||||
|
UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL("CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)")
|
||||||
|
db.execSQL("CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package org.thoughtcrime.securesms.keyvalue
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values for managing enable/disable state and corresponding alerts for Notification Profiles.
|
||||||
|
*/
|
||||||
|
internal class NotificationProfileValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_LAST_PROFILE_POPUP = "np.last_profile_popup"
|
||||||
|
private const val KEY_LAST_PROFILE_POPUP_TIME = "np.last_profile_popup_time"
|
||||||
|
private const val KEY_SEEN_TOOLTIP = "np.seen_tooltip"
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
const val KEY_MANUALLY_ENABLED_PROFILE = "np.manually_enabled_profile"
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
const val KEY_MANUALLY_ENABLED_UNTIL = "np.manually_enabled_until"
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
const val KEY_MANUALLY_DISABLED_AT = "np.manually_disabled_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFirstEverAppLaunch() = Unit
|
||||||
|
|
||||||
|
override fun getKeysToIncludeInBackup(): MutableList<String> {
|
||||||
|
return mutableListOf(KEY_SEEN_TOOLTIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
var manuallyEnabledProfile: Long by longValue(KEY_MANUALLY_ENABLED_PROFILE, 0L)
|
||||||
|
var manuallyEnabledUntil: Long by longValue(KEY_MANUALLY_ENABLED_UNTIL, 0L)
|
||||||
|
var manuallyDisabledAt: Long by longValue(KEY_MANUALLY_DISABLED_AT, 0L)
|
||||||
|
|
||||||
|
var lastProfilePopup: Long by longValue(KEY_LAST_PROFILE_POPUP, 0L)
|
||||||
|
var lastProfilePopupTime: Long by longValue(KEY_LAST_PROFILE_POPUP_TIME, 0L)
|
||||||
|
var hasSeenTooltip: Boolean by booleanValue(KEY_SEEN_TOOLTIP, false)
|
||||||
|
}
|
|
@ -18,28 +18,29 @@ public final class SignalStore {
|
||||||
|
|
||||||
private KeyValueStore store;
|
private KeyValueStore store;
|
||||||
|
|
||||||
private final AccountValues accountValues;
|
private final AccountValues accountValues;
|
||||||
private final KbsValues kbsValues;
|
private final KbsValues kbsValues;
|
||||||
private final RegistrationValues registrationValues;
|
private final RegistrationValues registrationValues;
|
||||||
private final PinValues pinValues;
|
private final PinValues pinValues;
|
||||||
private final RemoteConfigValues remoteConfigValues;
|
private final RemoteConfigValues remoteConfigValues;
|
||||||
private final StorageServiceValues storageServiceValues;
|
private final StorageServiceValues storageServiceValues;
|
||||||
private final UiHints uiHints;
|
private final UiHints uiHints;
|
||||||
private final TooltipValues tooltipValues;
|
private final TooltipValues tooltipValues;
|
||||||
private final MiscellaneousValues misc;
|
private final MiscellaneousValues misc;
|
||||||
private final InternalValues internalValues;
|
private final InternalValues internalValues;
|
||||||
private final EmojiValues emojiValues;
|
private final EmojiValues emojiValues;
|
||||||
private final SettingsValues settingsValues;
|
private final SettingsValues settingsValues;
|
||||||
private final CertificateValues certificateValues;
|
private final CertificateValues certificateValues;
|
||||||
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
|
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
|
||||||
private final OnboardingValues onboardingValues;
|
private final OnboardingValues onboardingValues;
|
||||||
private final WallpaperValues wallpaperValues;
|
private final WallpaperValues wallpaperValues;
|
||||||
private final PaymentsValues paymentsValues;
|
private final PaymentsValues paymentsValues;
|
||||||
private final DonationsValues donationsValues;
|
private final DonationsValues donationsValues;
|
||||||
private final ProxyValues proxyValues;
|
private final ProxyValues proxyValues;
|
||||||
private final RateLimitValues rateLimitValues;
|
private final RateLimitValues rateLimitValues;
|
||||||
private final ChatColorsValues chatColorsValues;
|
private final ChatColorsValues chatColorsValues;
|
||||||
private final ImageEditorValues imageEditorValues;
|
private final ImageEditorValues imageEditorValues;
|
||||||
|
private final NotificationProfileValues notificationProfileValues;
|
||||||
|
|
||||||
private static volatile SignalStore instance;
|
private static volatile SignalStore instance;
|
||||||
|
|
||||||
|
@ -56,29 +57,30 @@ public final class SignalStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalStore(@NonNull KeyValueStore store) {
|
private SignalStore(@NonNull KeyValueStore store) {
|
||||||
this.store = store;
|
this.store = store;
|
||||||
this.accountValues = new AccountValues(store);
|
this.accountValues = new AccountValues(store);
|
||||||
this.kbsValues = new KbsValues(store);
|
this.kbsValues = new KbsValues(store);
|
||||||
this.registrationValues = new RegistrationValues(store);
|
this.registrationValues = new RegistrationValues(store);
|
||||||
this.pinValues = new PinValues(store);
|
this.pinValues = new PinValues(store);
|
||||||
this.remoteConfigValues = new RemoteConfigValues(store);
|
this.remoteConfigValues = new RemoteConfigValues(store);
|
||||||
this.storageServiceValues = new StorageServiceValues(store);
|
this.storageServiceValues = new StorageServiceValues(store);
|
||||||
this.uiHints = new UiHints(store);
|
this.uiHints = new UiHints(store);
|
||||||
this.tooltipValues = new TooltipValues(store);
|
this.tooltipValues = new TooltipValues(store);
|
||||||
this.misc = new MiscellaneousValues(store);
|
this.misc = new MiscellaneousValues(store);
|
||||||
this.internalValues = new InternalValues(store);
|
this.internalValues = new InternalValues(store);
|
||||||
this.emojiValues = new EmojiValues(store);
|
this.emojiValues = new EmojiValues(store);
|
||||||
this.settingsValues = new SettingsValues(store);
|
this.settingsValues = new SettingsValues(store);
|
||||||
this.certificateValues = new CertificateValues(store);
|
this.certificateValues = new CertificateValues(store);
|
||||||
this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store);
|
this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store);
|
||||||
this.onboardingValues = new OnboardingValues(store);
|
this.onboardingValues = new OnboardingValues(store);
|
||||||
this.wallpaperValues = new WallpaperValues(store);
|
this.wallpaperValues = new WallpaperValues(store);
|
||||||
this.paymentsValues = new PaymentsValues(store);
|
this.paymentsValues = new PaymentsValues(store);
|
||||||
this.donationsValues = new DonationsValues(store);
|
this.donationsValues = new DonationsValues(store);
|
||||||
this.proxyValues = new ProxyValues(store);
|
this.proxyValues = new ProxyValues(store);
|
||||||
this.rateLimitValues = new RateLimitValues(store);
|
this.rateLimitValues = new RateLimitValues(store);
|
||||||
this.chatColorsValues = new ChatColorsValues(store);
|
this.chatColorsValues = new ChatColorsValues(store);
|
||||||
this.imageEditorValues = new ImageEditorValues(store);
|
this.imageEditorValues = new ImageEditorValues(store);
|
||||||
|
this.notificationProfileValues = new NotificationProfileValues(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onFirstEverAppLaunch() {
|
public static void onFirstEverAppLaunch() {
|
||||||
|
@ -104,6 +106,7 @@ public final class SignalStore {
|
||||||
rateLimit().onFirstEverAppLaunch();
|
rateLimit().onFirstEverAppLaunch();
|
||||||
chatColorsValues().onFirstEverAppLaunch();
|
chatColorsValues().onFirstEverAppLaunch();
|
||||||
imageEditorValues().onFirstEverAppLaunch();
|
imageEditorValues().onFirstEverAppLaunch();
|
||||||
|
notificationProfileValues().onFirstEverAppLaunch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> getKeysToIncludeInBackup() {
|
public static List<String> getKeysToIncludeInBackup() {
|
||||||
|
@ -130,6 +133,7 @@ public final class SignalStore {
|
||||||
keys.addAll(rateLimit().getKeysToIncludeInBackup());
|
keys.addAll(rateLimit().getKeysToIncludeInBackup());
|
||||||
keys.addAll(chatColorsValues().getKeysToIncludeInBackup());
|
keys.addAll(chatColorsValues().getKeysToIncludeInBackup());
|
||||||
keys.addAll(imageEditorValues().getKeysToIncludeInBackup());
|
keys.addAll(imageEditorValues().getKeysToIncludeInBackup());
|
||||||
|
keys.addAll(notificationProfileValues().getKeysToIncludeInBackup());
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,6 +234,10 @@ public final class SignalStore {
|
||||||
return getInstance().imageEditorValues;
|
return getInstance().imageEditorValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull NotificationProfileValues notificationProfileValues() {
|
||||||
|
return getInstance().notificationProfileValues;
|
||||||
|
}
|
||||||
|
|
||||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||||
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
package org.thoughtcrime.securesms.keyvalue
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
internal fun SignalStoreValues.longValue(key: String, default: Long): SignalStoreValueDelegate<Long> {
|
||||||
|
return LongValue(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SignalStoreValues.booleanValue(key: String, default: Boolean): SignalStoreValueDelegate<Boolean> {
|
||||||
|
return BooleanValue(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T : String?> SignalStoreValues.stringValue(key: String, default: T): SignalStoreValueDelegate<T> {
|
||||||
|
return StringValue(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SignalStoreValues.integerValue(key: String, default: Int): SignalStoreValueDelegate<Int> {
|
||||||
|
return IntValue(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SignalStoreValues.floatValue(key: String, default: Float): SignalStoreValueDelegate<Float> {
|
||||||
|
return FloatValue(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SignalStoreValues.blobValue(key: String, default: ByteArray): SignalStoreValueDelegate<ByteArray> {
|
||||||
|
return BlobValue(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin delegate that serves as a base for all other value types. This allows us to only expose this sealed
|
||||||
|
* class to callers and protect the individual implementations as private behind the various extension functions.
|
||||||
|
*/
|
||||||
|
sealed class SignalStoreValueDelegate<T> {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||||
|
return getValue(thisRef as SignalStoreValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||||
|
setValue(thisRef as SignalStoreValues, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract fun getValue(values: SignalStoreValues): T
|
||||||
|
internal abstract fun setValue(values: SignalStoreValues, value: T)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LongValue(private val key: String, private val default: Long) : SignalStoreValueDelegate<Long>() {
|
||||||
|
override fun getValue(values: SignalStoreValues): Long {
|
||||||
|
return values.getLong(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(values: SignalStoreValues, value: Long) {
|
||||||
|
values.putLong(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BooleanValue(private val key: String, private val default: Boolean) : SignalStoreValueDelegate<Boolean>() {
|
||||||
|
override fun getValue(values: SignalStoreValues): Boolean {
|
||||||
|
return values.getBoolean(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(values: SignalStoreValues, value: Boolean) {
|
||||||
|
values.putBoolean(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StringValue<T : String?>(private val key: String, private val default: T) : SignalStoreValueDelegate<T>() {
|
||||||
|
override fun getValue(values: SignalStoreValues): T {
|
||||||
|
return values.getString(key, default) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(values: SignalStoreValues, value: T) {
|
||||||
|
values.putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IntValue(private val key: String, private val default: Int) : SignalStoreValueDelegate<Int>() {
|
||||||
|
override fun getValue(values: SignalStoreValues): Int {
|
||||||
|
return values.getInteger(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(values: SignalStoreValues, value: Int) {
|
||||||
|
values.putInteger(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FloatValue(private val key: String, private val default: Float) : SignalStoreValueDelegate<Float>() {
|
||||||
|
override fun getValue(values: SignalStoreValues): Float {
|
||||||
|
return values.getFloat(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(values: SignalStoreValues, value: Float) {
|
||||||
|
values.putFloat(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BlobValue(private val key: String, private val default: ByteArray) : SignalStoreValueDelegate<ByteArray>() {
|
||||||
|
override fun getValue(values: SignalStoreValues): ByteArray {
|
||||||
|
return values.getBlob(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(values: SignalStoreValues, value: ByteArray) {
|
||||||
|
values.putBlob(key, value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
|
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.LocaleFeatureFlags;
|
import org.thoughtcrime.securesms.util.LocaleFeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.PlayServicesUtil;
|
import org.thoughtcrime.securesms.util.PlayServicesUtil;
|
||||||
|
@ -110,6 +109,7 @@ public final class Megaphones {
|
||||||
put(Event.CHAT_COLORS, ALWAYS);
|
put(Event.CHAT_COLORS, ALWAYS);
|
||||||
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
|
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
|
||||||
put(Event.BECOME_A_SUSTAINER, shouldShowDonateMegaphone(context) ? ShowForDurationSchedule.showForDays(7) : NEVER);
|
put(Event.BECOME_A_SUSTAINER, shouldShowDonateMegaphone(context) ? ShowForDurationSchedule.showForDays(7) : NEVER);
|
||||||
|
put(Event.NOTIFICATION_PROFILES, ShowForDurationSchedule.showForDays(7));
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +141,8 @@ public final class Megaphones {
|
||||||
return buildAddAProfilePhotoMegaphone(context);
|
return buildAddAProfilePhotoMegaphone(context);
|
||||||
case BECOME_A_SUSTAINER:
|
case BECOME_A_SUSTAINER:
|
||||||
return buildBecomeASustainerMegaphone(context);
|
return buildBecomeASustainerMegaphone(context);
|
||||||
|
case NOTIFICATION_PROFILES:
|
||||||
|
return buildNotificationProfilesMegaphone(context);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Event not handled!");
|
throw new IllegalArgumentException("Event not handled!");
|
||||||
}
|
}
|
||||||
|
@ -342,6 +344,20 @@ public final class Megaphones {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static @NonNull Megaphone buildNotificationProfilesMegaphone(@NonNull Context context) {
|
||||||
|
return new Megaphone.Builder(Event.NOTIFICATION_PROFILES, Megaphone.Style.BASIC)
|
||||||
|
.setTitle(R.string.NotificationProfilesMegaphone__notification_profiles)
|
||||||
|
.setImage(R.drawable.ic_notification_profiles_megaphone)
|
||||||
|
.setBody(R.string.NotificationProfilesMegaphone__only_get_notifications_from_the_people_and_groups_you_choose)
|
||||||
|
.setActionButton(R.string.NotificationProfilesMegaphone__create_a_profile, (megaphone, listener) -> {
|
||||||
|
listener.onMegaphoneNavigationRequested(AppSettingsActivity.notificationProfiles(context));
|
||||||
|
listener.onMegaphoneCompleted(Event.NOTIFICATION_PROFILES);
|
||||||
|
})
|
||||||
|
.setSecondaryButton(R.string.NotificationProfilesMegaphone__not_now, (megaphone, listener) -> {
|
||||||
|
listener.onMegaphoneCompleted(Event.NOTIFICATION_PROFILES);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean shouldShowMessageRequestsMegaphone() {
|
private static boolean shouldShowMessageRequestsMegaphone() {
|
||||||
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
||||||
|
@ -420,7 +436,8 @@ public final class Megaphones {
|
||||||
NOTIFICATIONS("notifications"),
|
NOTIFICATIONS("notifications"),
|
||||||
CHAT_COLORS("chat_colors"),
|
CHAT_COLORS("chat_colors"),
|
||||||
ADD_A_PROFILE_PHOTO("add_a_profile_photo"),
|
ADD_A_PROFILE_PHOTO("add_a_profile_photo"),
|
||||||
BECOME_A_SUSTAINER("become_a_sustainer");
|
BECOME_A_SUSTAINER("become_a_sustainer"),
|
||||||
|
NOTIFICATION_PROFILES("notification_profiles");
|
||||||
|
|
||||||
private final String key;
|
private final String key;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.thoughtcrime.securesms.notifications.profiles
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
data class NotificationProfile(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val emoji: String,
|
||||||
|
val color: AvatarColor = AvatarColor.A210,
|
||||||
|
val createdAt: Long,
|
||||||
|
val allowAllCalls: Boolean = false,
|
||||||
|
val allowAllMentions: Boolean = false,
|
||||||
|
val schedule: NotificationProfileSchedule,
|
||||||
|
val allowedMembers: Set<RecipientId> = emptySet()
|
||||||
|
) : Comparable<NotificationProfile> {
|
||||||
|
|
||||||
|
fun isRecipientAllowed(id: RecipientId): Boolean {
|
||||||
|
return allowedMembers.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: NotificationProfile): Int {
|
||||||
|
return createdAt.compareTo(other.createdAt)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.thoughtcrime.securesms.notifications.profiles
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.isBetween
|
||||||
|
import org.thoughtcrime.securesms.util.toLocalDateTime
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulate when a notification should be active based on days of the week, start time,
|
||||||
|
* and end times.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* start: 9am end: 5pm daysEnabled: Monday would return true for times between Monday 9am and Monday 5pm
|
||||||
|
* start: 9pm end: 5am daysEnabled: Monday would return true for times between Monday 9pm and Tuesday 5am
|
||||||
|
* start: 12am end: 12am daysEnabled: Monday would return true for times between Monday 12am and Monday 11:59:59pm
|
||||||
|
*/
|
||||||
|
data class NotificationProfileSchedule(
|
||||||
|
val id: Long,
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
val start: Int = 900,
|
||||||
|
val end: Int = 1700,
|
||||||
|
val daysEnabled: Set<DayOfWeek> = emptySet()
|
||||||
|
) {
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun isCurrentlyActive(now: Long, zoneId: ZoneId = ZoneId.systemDefault()): Boolean {
|
||||||
|
if (!enabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return coversTime(now, zoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun coversTime(time: Long, zoneId: ZoneId = ZoneId.systemDefault()): Boolean {
|
||||||
|
val localNow: LocalDateTime = time.toLocalDateTime(zoneId)
|
||||||
|
val localStart: LocalDateTime = start.toLocalDateTime(localNow)
|
||||||
|
val localEnd: LocalDateTime = end.toLocalDateTime(localNow)
|
||||||
|
|
||||||
|
return if (end < start) {
|
||||||
|
(daysEnabled.contains(localStart.dayOfWeek.minus(1)) && localNow.isBetween(localStart.minusDays(1), localEnd)) ||
|
||||||
|
(daysEnabled.contains(localStart.dayOfWeek) && localNow.isBetween(localStart, localEnd.plusDays(1)))
|
||||||
|
} else {
|
||||||
|
daysEnabled.contains(localStart.dayOfWeek) && localNow.isBetween(localStart, localEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startTime(): LocalTime {
|
||||||
|
return LocalTime.of(start / 100, start % 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDateTime(now: LocalDateTime): LocalDateTime {
|
||||||
|
return start.toLocalDateTime(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun endTime(): LocalTime {
|
||||||
|
return LocalTime.of(end / 100, end % 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun endDateTime(now: LocalDateTime): LocalDateTime {
|
||||||
|
return end.toLocalDateTime(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.toLocalDateTime(now: LocalDateTime): LocalDateTime {
|
||||||
|
if (this == 2400) {
|
||||||
|
return now.plusDays(1).withHour(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return now.withHour(this / 100).withMinute(this % 100)
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package org.thoughtcrime.securesms.notifications.profiles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.NotificationProfileValues
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.formatHours
|
||||||
|
import org.thoughtcrime.securesms.util.toLocalDateTime
|
||||||
|
import org.thoughtcrime.securesms.util.toLocalTime
|
||||||
|
import org.thoughtcrime.securesms.util.toMillis
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for determining the single, currently active Notification Profile (if any) and also how to describe
|
||||||
|
* how long the active profile will be on for.
|
||||||
|
*/
|
||||||
|
object NotificationProfiles {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@JvmOverloads
|
||||||
|
fun getActiveProfile(profiles: List<NotificationProfile>, now: Long = System.currentTimeMillis(), zoneId: ZoneId = ZoneId.systemDefault()): NotificationProfile? {
|
||||||
|
val storeValues: NotificationProfileValues = SignalStore.notificationProfileValues()
|
||||||
|
val localNow: LocalDateTime = now.toLocalDateTime(zoneId)
|
||||||
|
|
||||||
|
val manualProfile: NotificationProfile? = profiles.firstOrNull { it.id == storeValues.manuallyEnabledProfile }
|
||||||
|
|
||||||
|
val scheduledProfile: NotificationProfile? = profiles.sortedDescending().filter { it.schedule.isCurrentlyActive(now, zoneId) }.firstOrNull { profile ->
|
||||||
|
profile.schedule.startDateTime(localNow).toMillis() > storeValues.manuallyDisabledAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manualProfile == null || scheduledProfile == null) {
|
||||||
|
return (if (now < storeValues.manuallyEnabledUntil) manualProfile else null) ?: scheduledProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (manualProfile == scheduledProfile) {
|
||||||
|
if (storeValues.manuallyEnabledUntil == Long.MAX_VALUE || now < storeValues.manuallyEnabledUntil) {
|
||||||
|
manualProfile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scheduledProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActiveProfileDescription(context: Context, profile: NotificationProfile, now: Long = System.currentTimeMillis()): String {
|
||||||
|
val storeValues: NotificationProfileValues = SignalStore.notificationProfileValues()
|
||||||
|
|
||||||
|
if (profile.id == storeValues.manuallyEnabledProfile) {
|
||||||
|
if (storeValues.manuallyEnabledUntil.isForever()) {
|
||||||
|
return context.getString(R.string.NotificationProfilesFragment__on)
|
||||||
|
} else if (now < storeValues.manuallyEnabledUntil) {
|
||||||
|
return context.getString(R.string.NotificationProfileSelection__on_until_s, storeValues.manuallyEnabledUntil.toLocalTime().formatHours())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getString(R.string.NotificationProfileSelection__on_until_s, profile.schedule.endTime().formatHours())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Long.isForever(): Boolean {
|
||||||
|
return this == Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,11 @@ import android.os.Build
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import me.leolin.shortcutbadger.ShortcutBadger
|
import me.leolin.shortcutbadger.ShortcutBadger
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase
|
import org.thoughtcrime.securesms.database.MessageDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
@ -22,6 +24,8 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier.ReminderReceiver
|
import org.thoughtcrime.securesms.notifications.MessageNotifier.ReminderReceiver
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper
|
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference
|
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||||
|
@ -48,12 +52,24 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
|
||||||
@Volatile private var previousLockedStatus: Boolean = KeyCachingService.isLocked(context)
|
@Volatile private var previousLockedStatus: Boolean = KeyCachingService.isLocked(context)
|
||||||
@Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy
|
@Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy
|
||||||
@Volatile private var previousState: NotificationStateV2 = NotificationStateV2.EMPTY
|
@Volatile private var previousState: NotificationStateV2 = NotificationStateV2.EMPTY
|
||||||
|
@Volatile private var notificationProfile: NotificationProfile? = null
|
||||||
|
@Volatile private var notificationProfileInitialized: Boolean = false
|
||||||
|
|
||||||
private val threadReminders: MutableMap<Long, Reminder> = ConcurrentHashMap()
|
private val threadReminders: MutableMap<Long, Reminder> = ConcurrentHashMap()
|
||||||
private val stickyThreads: MutableMap<Long, StickyThread> = mutableMapOf()
|
private val stickyThreads: MutableMap<Long, StickyThread> = mutableMapOf()
|
||||||
|
|
||||||
private val executor = CancelableExecutor()
|
private val executor = CancelableExecutor()
|
||||||
|
|
||||||
|
init {
|
||||||
|
NotificationProfilesRepository().getProfiles()
|
||||||
|
.subscribeBy(
|
||||||
|
onNext = {
|
||||||
|
notificationProfile = NotificationProfiles.getActiveProfile(it)
|
||||||
|
notificationProfileInitialized = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setVisibleThread(threadId: Long) {
|
override fun setVisibleThread(threadId: Long) {
|
||||||
visibleThread = threadId
|
visibleThread = threadId
|
||||||
stickyThreads.remove(threadId)
|
stickyThreads.remove(threadId)
|
||||||
|
@ -115,10 +131,6 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
|
||||||
reminderCount: Int,
|
reminderCount: Int,
|
||||||
defaultBubbleState: BubbleState
|
defaultBubbleState: BubbleState
|
||||||
) {
|
) {
|
||||||
if (!SignalStore.settings().isMessageNotificationsEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentLockStatus: Boolean = KeyCachingService.isLocked(context)
|
val currentLockStatus: Boolean = KeyCachingService.isLocked(context)
|
||||||
val currentPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy
|
val currentPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy
|
||||||
val notificationConfigurationChanged: Boolean = currentLockStatus != previousLockedStatus || currentPrivacyPreference != previousPrivacyPreference
|
val notificationConfigurationChanged: Boolean = currentLockStatus != previousLockedStatus || currentPrivacyPreference != previousPrivacyPreference
|
||||||
|
@ -129,10 +141,42 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
|
||||||
stickyThreads.clear()
|
stickyThreads.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!notificationProfileInitialized) {
|
||||||
|
notificationProfile = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles.getProfiles())
|
||||||
|
notificationProfileInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
Log.internal().i(TAG, "sticky thread: $stickyThreads")
|
Log.internal().i(TAG, "sticky thread: $stickyThreads")
|
||||||
var state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(context, stickyThreads)
|
var state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(stickyThreads, notificationProfile)
|
||||||
Log.internal().i(TAG, "state: $state")
|
Log.internal().i(TAG, "state: $state")
|
||||||
|
|
||||||
|
if (state.muteFilteredMessages.isNotEmpty()) {
|
||||||
|
Log.i(TAG, "Marking ${state.muteFilteredMessages.size} muted messages as notified to skip notification")
|
||||||
|
state.muteFilteredMessages.forEach { item ->
|
||||||
|
val messageDatabase: MessageDatabase = if (item.isMms) SignalDatabase.mms else SignalDatabase.sms
|
||||||
|
messageDatabase.markAsNotified(item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.profileFilteredMessages.isNotEmpty()) {
|
||||||
|
Log.i(TAG, "Marking ${state.profileFilteredMessages.size} profile filtered messages as notified to skip notification")
|
||||||
|
state.profileFilteredMessages.forEach { item ->
|
||||||
|
val messageDatabase: MessageDatabase = if (item.isMms) SignalDatabase.mms else SignalDatabase.sms
|
||||||
|
messageDatabase.markAsNotified(item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SignalStore.settings().isMessageNotificationsEnabled) {
|
||||||
|
Log.i(TAG, "Marking ${state.conversations.size} conversations as notified to skip notification")
|
||||||
|
state.conversations.forEach { conversation ->
|
||||||
|
conversation.notificationItems.forEach { item ->
|
||||||
|
val messageDatabase: MessageDatabase = if (item.isMms) SignalDatabase.mms else SignalDatabase.sms
|
||||||
|
messageDatabase.markAsNotified(item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val displayedNotifications: Set<Int>? = ServiceUtil.getNotificationManager(context).getDisplayedNotificationIds().getOrNull()
|
val displayedNotifications: Set<Int>? = ServiceUtil.getNotificationManager(context).getDisplayedNotificationIds().getOrNull()
|
||||||
if (displayedNotifications != null) {
|
if (displayedNotifications != null) {
|
||||||
val cleanedUpThreadIds: MutableSet<Long> = mutableSetOf()
|
val cleanedUpThreadIds: MutableSet<Long> = mutableSetOf()
|
||||||
|
@ -146,7 +190,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
|
||||||
}
|
}
|
||||||
if (cleanedUpThreadIds.isNotEmpty()) {
|
if (cleanedUpThreadIds.isNotEmpty()) {
|
||||||
Log.i(TAG, "Cleaned up ${cleanedUpThreadIds.size} thread(s) with dangling notifications")
|
Log.i(TAG, "Cleaned up ${cleanedUpThreadIds.size} thread(s) with dangling notifications")
|
||||||
state = NotificationStateV2(state.conversations.filterNot { cleanedUpThreadIds.contains(it.threadId) })
|
state = state.copy(conversations = state.conversations.filterNot { cleanedUpThreadIds.contains(it.threadId) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.thoughtcrime.securesms.notifications.v2
|
package org.thoughtcrime.securesms.notifications.v2
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||||
|
@ -10,9 +9,9 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.CursorUtil
|
import org.thoughtcrime.securesms.util.CursorUtil
|
||||||
import java.lang.IllegalStateException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries the message databases to determine messages that should be in notifications.
|
* Queries the message databases to determine messages that should be in notifications.
|
||||||
|
@ -22,7 +21,7 @@ object NotificationStateProvider {
|
||||||
private val TAG = Log.tag(NotificationStateProvider::class.java)
|
private val TAG = Log.tag(NotificationStateProvider::class.java)
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun constructNotificationState(context: Context, stickyThreads: Map<Long, MessageNotifierV2.StickyThread>): NotificationStateV2 {
|
fun constructNotificationState(stickyThreads: Map<Long, MessageNotifierV2.StickyThread>, notificationProfile: NotificationProfile?): NotificationStateV2 {
|
||||||
val messages: MutableList<NotificationMessage> = mutableListOf()
|
val messages: MutableList<NotificationMessage> = mutableListOf()
|
||||||
|
|
||||||
SignalDatabase.mmsSms.getMessagesForNotificationState(stickyThreads.values).use { unreadMessages ->
|
SignalDatabase.mmsSms.getMessagesForNotificationState(stickyThreads.values).use { unreadMessages ->
|
||||||
|
@ -60,18 +59,30 @@ object NotificationStateProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
val conversations: MutableList<NotificationConversation> = mutableListOf()
|
val conversations: MutableList<NotificationConversation> = mutableListOf()
|
||||||
|
val muteFilteredMessages: MutableList<NotificationStateV2.FilteredMessage> = mutableListOf()
|
||||||
|
val profileFilteredMessages: MutableList<NotificationStateV2.FilteredMessage> = mutableListOf()
|
||||||
|
|
||||||
messages.groupBy { it.threadId }
|
messages.groupBy { it.threadId }
|
||||||
.forEach { (threadId, threadMessages) ->
|
.forEach { (threadId, threadMessages) ->
|
||||||
var notificationItems: MutableList<NotificationItemV2> = mutableListOf()
|
var notificationItems: MutableList<NotificationItemV2> = mutableListOf()
|
||||||
|
|
||||||
for (notification: NotificationMessage in threadMessages) {
|
for (notification: NotificationMessage in threadMessages) {
|
||||||
if (notification.includeMessage()) {
|
when (notification.includeMessage(notificationProfile)) {
|
||||||
notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord))
|
MessageInclusion.INCLUDE -> notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord))
|
||||||
|
MessageInclusion.EXCLUDE -> Unit
|
||||||
|
MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
|
||||||
|
MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.hasUnreadReactions) {
|
if (notification.hasUnreadReactions) {
|
||||||
notification.reactions.filter { notification.includeReaction(it) }
|
notification.reactions.forEach {
|
||||||
.forEach { notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it)) }
|
when (notification.includeReaction(it, notificationProfile)) {
|
||||||
|
MessageInclusion.INCLUDE -> notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it))
|
||||||
|
MessageInclusion.EXCLUDE -> Unit
|
||||||
|
MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
|
||||||
|
MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +97,7 @@ object NotificationStateProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NotificationStateV2(conversations)
|
return NotificationStateV2(conversations, muteFilteredMessages, profileFilteredMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class NotificationMessage(
|
private data class NotificationMessage(
|
||||||
|
@ -101,18 +112,40 @@ object NotificationStateProvider {
|
||||||
) {
|
) {
|
||||||
private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing
|
private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing
|
||||||
|
|
||||||
fun includeMessage(): Boolean {
|
fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion {
|
||||||
return (isUnreadIncoming || stickyThread) && (threadRecipient.isNotMuted || (threadRecipient.isAlwaysNotifyMentions && messageRecord.hasSelfMention()))
|
return if (isUnreadIncoming || stickyThread) {
|
||||||
|
if (threadRecipient.isMuted && (threadRecipient.isDoNotNotifyMentions || !messageRecord.hasSelfMention())) {
|
||||||
|
MessageInclusion.MUTE_FILTERED
|
||||||
|
} else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasSelfMention())) {
|
||||||
|
MessageInclusion.PROFILE_FILTERED
|
||||||
|
} else {
|
||||||
|
MessageInclusion.INCLUDE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessageInclusion.EXCLUDE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun includeReaction(reaction: ReactionRecord): Boolean {
|
fun includeReaction(reaction: ReactionRecord, notificationProfile: NotificationProfile?): MessageInclusion {
|
||||||
return reaction.author != Recipient.self().id && messageRecord.isOutgoing && reaction.dateReceived > lastReactionRead && threadRecipient.isNotMuted
|
return if (threadRecipient.isMuted) {
|
||||||
|
MessageInclusion.MUTE_FILTERED
|
||||||
|
} else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id)) {
|
||||||
|
MessageInclusion.PROFILE_FILTERED
|
||||||
|
} else if (reaction.author != Recipient.self().id && messageRecord.isOutgoing && reaction.dateReceived > lastReactionRead) {
|
||||||
|
MessageInclusion.INCLUDE
|
||||||
|
} else {
|
||||||
|
MessageInclusion.EXCLUDE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Recipient.isNotMuted: Boolean
|
private val Recipient.isDoNotNotifyMentions: Boolean
|
||||||
get() = !isMuted
|
get() = mentionSetting == RecipientDatabase.MentionSetting.DO_NOT_NOTIFY
|
||||||
|
}
|
||||||
|
|
||||||
private val Recipient.isAlwaysNotifyMentions: Boolean
|
private enum class MessageInclusion {
|
||||||
get() = mentionSetting == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY
|
INCLUDE,
|
||||||
|
EXCLUDE,
|
||||||
|
MUTE_FILTERED,
|
||||||
|
PROFILE_FILTERED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
/**
|
/**
|
||||||
* Hold all state for notifications for all conversations.
|
* Hold all state for notifications for all conversations.
|
||||||
*/
|
*/
|
||||||
data class NotificationStateV2(val conversations: List<NotificationConversation>) {
|
data class NotificationStateV2(val conversations: List<NotificationConversation>, val muteFilteredMessages: List<FilteredMessage>, val profileFilteredMessages: List<FilteredMessage>) {
|
||||||
|
|
||||||
val threadCount: Int = conversations.size
|
val threadCount: Int = conversations.size
|
||||||
val isEmpty: Boolean = conversations.isEmpty()
|
val isEmpty: Boolean = conversations.isEmpty()
|
||||||
|
@ -91,7 +91,9 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FilteredMessage(val id: Long, val isMms: Boolean)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = NotificationStateV2(emptyList())
|
val EMPTY = NotificationStateV2(emptyList(), emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||||
|
@ -104,6 +106,18 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationProfile activeProfile = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles().getProfiles());
|
||||||
|
if (activeProfile != null && !(activeProfile.isRecipientAllowed(remotePeerGroup.getId()) || activeProfile.getAllowAllCalls())) {
|
||||||
|
try {
|
||||||
|
Log.i(TAG, "Incoming ring request for profile restricted recipient");
|
||||||
|
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
|
||||||
|
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.DeclinedByUser);
|
||||||
|
} catch (CallException e) {
|
||||||
|
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
|
||||||
|
}
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
webRtcInteractor.peekGroupCallForRingingCheck(new GroupCallRingCheckInfo(remotePeerGroup.getId(), groupId, ringId, uuid, ringUpdate));
|
webRtcInteractor.peekGroupCallForRingingCheck(new GroupCallRingCheckInfo(remotePeerGroup.getId(), groupId, ringId, uuid, ringUpdate));
|
||||||
|
|
||||||
return currentState;
|
return currentState;
|
||||||
|
|
|
@ -24,6 +24,8 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
|
@ -187,6 +189,14 @@ public abstract class WebRtcActionProcessor {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationProfile activeProfile = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles().getProfiles());
|
||||||
|
if (activeProfile != null && !(activeProfile.isRecipientAllowed(callMetadata.getRemotePeer().getId()) || activeProfile.getAllowAllCalls())) {
|
||||||
|
Log.w(tag, "Caller is excluded by notification profile.");
|
||||||
|
currentState = currentState.getActionProcessor().handleSendHangup(currentState, callMetadata, WebRtcData.HangupMetadata.fromType(HangupMessage.Type.NORMAL), true);
|
||||||
|
webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), receivedOfferMetadata.getServerReceivedTimestamp(), offerMetadata.getOfferType() == OfferMessage.Type.VIDEO_CALL);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(tag, "add remotePeer callId: " + callMetadata.getRemotePeer().getCallId() + " key: " + callMetadata.getRemotePeer().hashCode());
|
Log.i(tag, "add remotePeer callId: " + callMetadata.getRemotePeer().getCallId() + " key: " + callMetadata.getRemotePeer().hashCode());
|
||||||
|
|
||||||
callMetadata.getRemotePeer().setCallStartTimestamp(receivedOfferMetadata.getServerReceivedTimestamp());
|
callMetadata.getRemotePeer().setCallStartTimestamp(receivedOfferMetadata.getServerReceivedTimestamp());
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert [LocalDateTime] to be same as [System.currentTimeMillis]
|
||||||
|
*/
|
||||||
|
fun LocalDateTime.toMillis(): Long {
|
||||||
|
return TimeUnit.SECONDS.toMillis(toEpochSecond(ZoneOffset.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the [LocalDateTime] is within [start] and [end] inclusive.
|
||||||
|
*/
|
||||||
|
fun LocalDateTime.isBetween(start: LocalDateTime, end: LocalDateTime): Boolean {
|
||||||
|
return (isEqual(start) || isAfter(start)) && (isEqual(end) || isBefore(end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert milliseconds to local date time with provided [zoneId].
|
||||||
|
*/
|
||||||
|
fun Long.toLocalDateTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalDateTime {
|
||||||
|
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), zoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts milliseconds to local time with provided [zoneId].
|
||||||
|
*/
|
||||||
|
fun Long.toLocalTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalTime {
|
||||||
|
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), zoneId).toLocalTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats [LocalTime] as localized time. For example, "8:00 AM"
|
||||||
|
*/
|
||||||
|
fun LocalTime.formatHours(): String {
|
||||||
|
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this)
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import android.widget.TextView
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a "toast" like message with text and icon that animates in from the top and then animates out to the top.
|
||||||
|
*/
|
||||||
|
class TopToastPopup private constructor(parent: ViewGroup, iconResource: Int, descriptionText: String) : PopupWindow(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.top_toast_popup, parent, false),
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewUtil.dpToPx(86)
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val icon: ImageView = contentView.findViewById(R.id.top_toast_popup_icon)
|
||||||
|
private val description: TextView = contentView.findViewById(R.id.top_toast_popup_description)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
elevation = ViewUtil.dpToPx(8).toFloat()
|
||||||
|
}
|
||||||
|
animationStyle = R.style.PopupAnimation
|
||||||
|
icon.setImageResource(iconResource)
|
||||||
|
description.text = descriptionText
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun show(parent: ViewGroup) {
|
||||||
|
showAtLocation(parent, Gravity.TOP or Gravity.START, 0, 0)
|
||||||
|
measureChild()
|
||||||
|
update()
|
||||||
|
contentView.postDelayed({ dismiss() }, DURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun measureChild() {
|
||||||
|
contentView.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DURATION = TimeUnit.SECONDS.toMillis(2)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun show(parent: ViewGroup, icon: Int, description: String): TopToastPopup {
|
||||||
|
val topToast = TopToastPopup(parent, icon, description)
|
||||||
|
topToast.show(parent)
|
||||||
|
return topToast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/signal_accent_primary" android:state_checked="true" android:state_enabled="true" />
|
||||||
|
<item android:color="@color/signal_background_secondary" android:state_checked="false" android:state_enabled="false" />
|
||||||
|
<item android:color="@color/signal_divider_minor" />
|
||||||
|
</selector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/signal_button_primary_text" android:state_checked="true" android:state_enabled="true" />
|
||||||
|
<item android:color="@color/signal_text_hint"/>
|
||||||
|
</selector>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/signal_text_primary_disabled" android:state_enabled="false" />
|
<item android:color="@color/signal_text_hint" android:state_enabled="false" />
|
||||||
<item android:color="?colorAccent" />
|
<item android:color="?colorAccent" />
|
||||||
</selector>
|
</selector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:fillColor="#DEDEDE"
|
||||||
|
android:pathData="M10,15.5l-8,-7.979l1.059,-1.062l6.941,6.923l6.941,-6.923l1.059,1.062l-8,7.979z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.9114,10.8453C14.0127,13.1367 11.3924,15 8.3418,15C4.1899,15 1,12.0288 1,7.8867C1,4.9406 2.7595,2.1205 5.3797,1.0881C5.5443,1.0126 5.7089,1 5.7975,1C6.0633,1 6.2025,1.214 6.2025,1.4029C6.2025,1.4658 6.2025,1.5666 6.1266,1.7302C5.8987,2.3219 5.6835,3.4425 5.6835,4.2356C5.6835,8.063 8.1392,10.455 11.9747,10.455C12.8481,10.455 13.7468,10.2662 14.2785,10.0899C14.4051,10.0396 14.4937,10.027 14.5823,10.027C14.7848,10.027 15,10.2032 15,10.4676C15,10.5306 14.9873,10.6942 14.9114,10.8453Z"
|
||||||
|
android:fillColor="#8B8BF9"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#8B8BF9"
|
||||||
|
android:pathData="M21.5,15.9c-1.2,3.1 -4.8,5.7 -9,5.7c-5.7,0 -10.1,-4.1 -10.1,-9.8c0,-4 2.4,-7.9 6,-9.3C8.6,2.4 8.9,2.4 9,2.4c0.4,0 0.6,0.3 0.6,0.6c0,0.1 0,0.2 -0.1,0.4C9.1,4.2 8.8,5.7 8.8,6.8c0,5.2 3.4,8.5 8.6,8.5c1.2,0 2.4,-0.3 3.2,-0.5c0.2,-0.1 0.3,-0.1 0.4,-0.1c0.3,0 0.6,0.2 0.6,0.6C21.6,15.5 21.6,15.7 21.5,15.9z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="88dp"
|
||||||
|
android:height="88dp"
|
||||||
|
android:viewportWidth="88"
|
||||||
|
android:viewportHeight="88">
|
||||||
|
<path
|
||||||
|
android:pathData="M44,44m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0"
|
||||||
|
android:fillColor="#303030"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M61.7722,51.3165C59.4611,57.2086 52.7233,62 44.8788,62C34.2025,62 26,54.3597 26,43.7086C26,36.1331 30.5244,28.8813 37.2622,26.2266C37.6854,26.0324 38.1085,26 38.3363,26C39.0199,26 39.3779,26.5504 39.3779,27.036C39.3779,27.1978 39.3779,27.4568 39.1826,27.8777C38.5967,29.3993 38.0434,32.2806 38.0434,34.3201C38.0434,44.1619 44.358,50.3129 54.2206,50.3129C56.4665,50.3129 58.7776,49.8273 60.1447,49.3741C60.4702,49.2446 60.698,49.2122 60.9259,49.2122C61.4467,49.2122 62,49.6655 62,50.3453C62,50.5072 61.9674,50.9281 61.7722,51.3165Z"
|
||||||
|
android:fillColor="#8B8BF9"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<ripple android:color="@color/signal_button_secondary">
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#000000" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:width="1.5dp" android:color="@color/signal_inverse_primary" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<ripple android:color="@color/signal_button_secondary">
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#000000" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:width="1.5dp" android:color="@color/signal_background_dialog_secondary" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
|
</item>
|
||||||
|
</selector>
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 6.8 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/media_overview_toolbar_secondary_background" />
|
||||||
|
<padding android:left="4dp" android:top="4dp" android:right="4dp" android:bottom="4dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:drawable="@drawable/ic_chevron_down_themed_20" />
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M10,15.5l-8,-7.979l1.059,-1.062l6.941,6.923l6.941,-6.923l1.059,1.062l-8,7.979z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:pathData="M10,10m-9.25,0a9.25,9.25 0,1 1,18.5 0a9.25,9.25 0,1 1,-18.5 0"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#848484"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M5.5,10H14.5"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#848484"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.9114,10.8453C14.0127,13.1367 11.3924,15 8.3418,15C4.1899,15 1,12.0288 1,7.8867C1,4.9406 2.7595,2.1205 5.3797,1.0881C5.5443,1.0126 5.7089,1 5.7975,1C6.0633,1 6.2025,1.214 6.2025,1.4029C6.2025,1.4658 6.2025,1.5666 6.1266,1.7302C5.8987,2.3219 5.6835,3.4425 5.6835,4.2356C5.6835,8.063 8.1392,10.455 11.9747,10.455C12.8481,10.455 13.7468,10.2662 14.2785,10.0899C14.4051,10.0396 14.4937,10.027 14.5823,10.027C14.7848,10.027 15,10.2032 15,10.4676C15,10.5306 14.9873,10.6942 14.9114,10.8453Z"
|
||||||
|
android:fillColor="#5151F6"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#5151F6"
|
||||||
|
android:pathData="M21.5,15.9c-1.2,3.1 -4.8,5.7 -9,5.7c-5.7,0 -10.1,-4.1 -10.1,-9.8c0,-4 2.4,-7.9 6,-9.3C8.6,2.4 8.9,2.4 9,2.4c0.4,0 0.6,0.3 0.6,0.6c0,0.1 0,0.2 -0.1,0.4C9.1,4.2 8.8,5.7 8.8,6.8c0,5.2 3.4,8.5 8.6,8.5c1.2,0 2.4,-0.3 3.2,-0.5c0.2,-0.1 0.3,-0.1 0.4,-0.1c0.3,0 0.6,0.2 0.6,0.6C21.6,15.5 21.6,15.7 21.5,15.9z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/signal_background_tertiary"/>
|
||||||
|
<corners android:radius="39dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:drawable="@drawable/ic_moon_16" android:left="13dp" android:right="13dp" android:top="5dp" android:bottom="5dp" />
|
||||||
|
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="88dp"
|
||||||
|
android:height="88dp"
|
||||||
|
android:viewportWidth="88"
|
||||||
|
android:viewportHeight="88">
|
||||||
|
<path
|
||||||
|
android:pathData="M44,44m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0"
|
||||||
|
android:fillColor="#E8E8FE"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M61.7722,51.3165C59.4611,57.2086 52.7233,62 44.8788,62C34.2025,62 26,54.3597 26,43.7086C26,36.1331 30.5244,28.8813 37.2622,26.2266C37.6854,26.0324 38.1085,26 38.3363,26C39.0199,26 39.3779,26.5504 39.3779,27.036C39.3779,27.1978 39.3779,27.4568 39.1826,27.8777C38.5967,29.3993 38.0434,32.2806 38.0434,34.3201C38.0434,44.1619 44.358,50.3129 54.2206,50.3129C56.4665,50.3129 58.7776,49.8273 60.1447,49.3741C60.4702,49.2446 60.698,49.2122 60.9259,49.2122C61.4467,49.2122 62,49.6655 62,50.3453C62,50.5072 61.9674,50.9281 61.7722,51.3165Z"
|
||||||
|
android:fillColor="#5151F6"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#E3E3FE"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:drawable="@drawable/ic_sleeping_face" android:top="12dp" android:bottom="12dp" android:left="12dp" android:right="12dp" />
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/signal_background_dialog_secondary" />
|
||||||
|
<padding android:right="12dp" android:bottom="12dp" android:top="12dp" android:left="12dp" />
|
||||||
|
<padding android:right="12dp" android:bottom="12dp" android:top="12dp" android:left="12dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:drawable="@drawable/ic_plus_24" />
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true" android:state_selected="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/signal_background_dialog_secondary" />
|
||||||
|
<stroke android:color="@color/signal_inverse_primary" android:width="1.5dp" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:color="@color/signal_inverse_primary" android:width="1.5dp" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/signal_background_dialog_secondary" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:color="@color/signal_background_dialog_secondary" android:width="1.5dp" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/top_toast_background_color" />
|
||||||
|
<corners android:radius="45dp" />
|
||||||
|
</shape>
|
|
@ -79,6 +79,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
|
android:textAllCaps="false"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/basic_megaphone_secondary"
|
app:layout_constraintStart_toEndOf="@id/basic_megaphone_secondary"
|
||||||
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
||||||
|
@ -92,6 +93,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:text="@string/Megaphones_remind_me_later"
|
android:text="@string/Megaphones_remind_me_later"
|
||||||
|
android:textAllCaps="false"
|
||||||
app:layout_constraintEnd_toStartOf="@id/basic_megaphone_action"
|
app:layout_constraintEnd_toStartOf="@id/basic_megaphone_action"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
||||||
|
|
|
@ -86,10 +86,25 @@
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/conversation_list_proxy_status"
|
app:layout_constraintEnd_toStartOf="@id/conversation_list_notification_profile_status"
|
||||||
app:layout_constraintStart_toEndOf="@id/toolbar_icon"
|
app:layout_constraintStart_toEndOf="@id/toolbar_icon"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/conversation_list_notification_profile_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:paddingHorizontal="3dp"
|
||||||
|
android:paddingVertical="11dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/conversation_list_proxy_status"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/conversation_list_title"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_notification_profile_active"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/conversation_list_proxy_status"
|
android:id="@+id/conversation_list_proxy_status"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -99,7 +114,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/search_action"
|
app:layout_constraintEnd_toStartOf="@id/search_action"
|
||||||
app:layout_constraintStart_toEndOf="@id/conversation_list_title"
|
app:layout_constraintStart_toEndOf="@id/conversation_list_notification_profile_status"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@drawable/ic_proxy_connected_24"
|
tools:src="@drawable/ic_proxy_connected_24"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:theme="?attr/settingsToolbarStyle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navigationContentDescription="@string/DSLSettingsToolbar__navigate_up"
|
||||||
|
app:navigationIcon="@drawable/ic_arrow_left_24"
|
||||||
|
app:titleTextAppearance="@style/Signal.Text.Title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/AddAllowedMembers__allowed_notifications"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/AddAllowedMembers__add_people_and_groups_you_want_notifications_and_calls_from_when_this_profile_is_on"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_title" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="60dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_notification_profile_schedule_description" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/toolbar_shadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:alpha="0"
|
||||||
|
android:background="@drawable/toolbar_shadow"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/recycler" />
|
||||||
|
|
||||||
|
<com.dd.CircularProgressButton
|
||||||
|
android:id="@+id/add_allowed_members_profile_next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:text="@string/EditNotificationProfileFragment__next"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:cornerRadius="80dp"
|
||||||
|
app:cpb_colorIndicator="@color/white"
|
||||||
|
app:cpb_colorProgress="?colorAccent"
|
||||||
|
app:cpb_cornerRadius="28dp"
|
||||||
|
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||||
|
app:cpb_textIdle="@string/EditNotificationProfileFragment__next"
|
||||||
|
app:elevation="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:theme="?attr/settingsToolbarStyle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navigationContentDescription="@string/DSLSettingsToolbar__navigate_up"
|
||||||
|
app:navigationIcon="@drawable/ic_arrow_left_24"
|
||||||
|
app:titleTextAppearance="@style/Signal.Text.Title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
|
tools:text="@string/EditNotificationProfileFragment__name_your_profile" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/edit_notification_profile_emoji"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/edit_notification_profile_name_wrapper"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/edit_notification_profile_name_wrapper"
|
||||||
|
app:srcCompat="@drawable/ic_add_emoji"
|
||||||
|
app:tint="@color/signal_text_secondary" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/edit_notification_profile_name_wrapper"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:hint="@string/EditNotificationProfileFragment__profile_name"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/edit_notification_profile_clear"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/edit_notification_profile_emoji"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_notification_profile_title">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
|
||||||
|
android:id="@+id/edit_notification_profile_name"
|
||||||
|
style="@style/Signal.Text.Body"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textCapSentences"
|
||||||
|
android:maxLines="1"
|
||||||
|
app:emoji_forceCustom="true" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/edit_notification_profile_clear"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/edit_notification_profile_name_wrapper"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/edit_notification_profile_name_wrapper"
|
||||||
|
app:srcCompat="@drawable/ic_x"
|
||||||
|
app:tint="@color/signal_text_secondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_count"
|
||||||
|
style="@style/Signal.Text.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/edit_notification_profile_name_wrapper"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_notification_profile_name_wrapper"
|
||||||
|
tools:text="75/100" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="60dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_notification_profile_count" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/toolbar_shadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:alpha="0"
|
||||||
|
android:background="@drawable/toolbar_shadow"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/recycler" />
|
||||||
|
|
||||||
|
<com.dd.CircularProgressButton
|
||||||
|
android:id="@+id/edit_notification_profile_save"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:text="@string/EditProfileNameFragment_save"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:cornerRadius="80dp"
|
||||||
|
app:cpb_colorIndicator="@color/white"
|
||||||
|
app:cpb_colorProgress="?colorAccent"
|
||||||
|
app:cpb_cornerRadius="28dp"
|
||||||
|
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||||
|
app:cpb_textIdle="@string/EditNotificationProfileFragment__next"
|
||||||
|
app:elevation="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,261 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout 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="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include layout="@layout/dsl_settings_toolbar" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fillViewport="true"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__add_a_schedule"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__turn_on_and_edit_your_schedule_to_automate_this_profile"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_switch_description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:minHeight="56dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__schedule"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/edit_notification_profile_schedule_switch"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_description"
|
||||||
|
app:layout_goneMarginTop="0dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:enabled="false"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/edit_notification_profile_schedule_switch_description"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/edit_notification_profile_schedule_switch_description" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_start"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__start"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_switch_description" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_end"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__end"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/edit_notification_profile_schedule_start"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/edit_notification_profile_schedule_end_time"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/edit_notification_profile_schedule_start" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_start_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:enabled="false"
|
||||||
|
android:letterSpacing="0.02"
|
||||||
|
android:textColor="@color/text_color_accent_enabled_selector"
|
||||||
|
android:textSize="36sp"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/edit_notification_profile_schedule_start"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_start"
|
||||||
|
tools:text="9:00" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_end_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:enabled="false"
|
||||||
|
android:letterSpacing="0.02"
|
||||||
|
android:textColor="@color/text_color_accent_enabled_selector"
|
||||||
|
android:textSize="36sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/edit_notification_profile_schedule_start_time"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/edit_notification_profile_schedule_start_time"
|
||||||
|
tools:text="5:00" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:constraint_referenced_ids="edit_notification_profile_schedule_sunday,edit_notification_profile_schedule_monday,edit_notification_profile_schedule_tuesday,edit_notification_profile_schedule_wednesday,edit_notification_profile_schedule_thursday,edit_notification_profile_schedule_friday,edit_notification_profile_schedule_saturday"
|
||||||
|
app:flow_horizontalStyle="spread_inside"
|
||||||
|
app:flow_wrapMode="aligned"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_start_time" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_sunday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__sunday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_monday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__monday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_tuesday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__tuesday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_wednesday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__wednesday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_thursday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__thursday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_friday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__friday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/edit_notification_profile_schedule_saturday"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/circle_tintable_padded"
|
||||||
|
android:enabled="false"
|
||||||
|
android:foreground="?selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:text="@string/EditNotificationProfileSchedule__saturday_first_letter"
|
||||||
|
android:textAlignment="gravity"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/notification_profile_schedule_text_selector" />
|
||||||
|
|
||||||
|
<com.dd.CircularProgressButton
|
||||||
|
android:id="@+id/edit_notification_profile_schedule__next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginBottom="@dimen/dsl_settings_gutter"
|
||||||
|
android:enabled="false"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:text="@string/EditNotificationProfileFragment__next"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:cornerRadius="80dp"
|
||||||
|
app:cpb_colorIndicator="@color/white"
|
||||||
|
app:cpb_colorProgress="?colorAccent"
|
||||||
|
app:cpb_cornerRadius="28dp"
|
||||||
|
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||||
|
app:cpb_textIdle="@string/EditNotificationProfileFragment__next"
|
||||||
|
app:elevation="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView 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="match_parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/notification_profile_created_icon"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="88dp"
|
||||||
|
android:layout_marginTop="176dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_notification_profile_done"
|
||||||
|
tools:backgroundTint="@color/blue_200" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_profile_created_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/NotificationProfileCreated__profile_created"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profile_created_icon" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/notification_profile_created_top_image"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:background="@drawable/tinted_circle"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:backgroundTint="@color/signal_background_secondary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profile_created_title"
|
||||||
|
app:tint="@color/signal_icon_tint_primary"
|
||||||
|
tools:srcCompat="@drawable/ic_recent_20" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_profile_created_top_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/notification_profile_created_top_image"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/notification_profile_created_top_image"
|
||||||
|
tools:text="@string/NotificationProfileCreated__your_profile_will_turn_on_and_off_automatically_according_to_your_schedule" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/notification_profile_created_top_barrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:constraint_referenced_ids="notification_profile_created_top_image,notification_profile_created_top_text" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/notification_profile_created_bottom_image"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:background="@drawable/tinted_circle"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:backgroundTint="@color/signal_background_secondary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profile_created_top_barrier"
|
||||||
|
app:tint="@color/signal_icon_tint_primary"
|
||||||
|
tools:srcCompat="@drawable/ic_more_vert_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_profile_created_bottom_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/notification_profile_created_bottom_image"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/notification_profile_created_bottom_image"
|
||||||
|
tools:text="@string/NotificationProfileCreated__you_can_turn_your_profile_on_or_off_manually_via_the_menu_on_the_chat_list" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/notification_profile_created_bottom_barrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:constraint_referenced_ids="notification_profile_created_bottom_image,notification_profile_created_bottom_text" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/notification_profile_created_done"
|
||||||
|
style="@style/Signal.Widget.Button.Large.Primary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:text="@string/NotificationProfileCreated__done"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profile_created_bottom_barrier"
|
||||||
|
app:layout_constraintVertical_bias="1" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include layout="@layout/dsl_settings_toolbar" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include layout="@layout/dsl_settings_toolbar" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.ContactFilterView
|
||||||
|
android:id="@+id/contact_filter_edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:minHeight="44dp"
|
||||||
|
app:cfv_autoFocus="false"
|
||||||
|
app:cfv_background="@drawable/rounded_rectangle_dialog_secondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/contact_selection_list_fragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.ContactSelectionListFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/contact_filter_edit_text" />
|
||||||
|
|
||||||
|
<com.dd.CircularProgressButton
|
||||||
|
android:id="@+id/select_recipients_add"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:text="@string/SelectRecipientsFragment__add"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:cornerRadius="80dp"
|
||||||
|
app:cpb_colorIndicator="@color/white"
|
||||||
|
app:cpb_colorProgress="?colorAccent"
|
||||||
|
app:cpb_cornerRadius="28dp"
|
||||||
|
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||||
|
app:cpb_textIdle="@string/SelectRecipientsFragment__add"
|
||||||
|
app:elevation="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:minHeight="72dp"
|
||||||
|
android:text="@string/NotificationProfileSelection__new_notification_profile"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
android:textColor="@color/signal_text_primary_dialog"
|
||||||
|
app:cornerRadius="18dp"
|
||||||
|
app:icon="@drawable/new_notification_profile_pref_icon"
|
||||||
|
app:iconGravity="start"
|
||||||
|
app:iconPadding="12dp"
|
||||||
|
app:iconTint="@null"
|
||||||
|
app:strokeColor="@color/signal_background_dialog_secondary"
|
||||||
|
app:strokeWidth="1.5dp" />
|
|
@ -0,0 +1,131 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout 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:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:background="@drawable/notification_entry_pref_background">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="72dp">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
|
android:id="@+id/notification_preference_image"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:background="@drawable/circle_tintable"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="12dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/notification_preference_collapse_barrier"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@drawable/ic_profile_circle_outline_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_preference_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/notification_preference_status"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/notification_preference_chevron"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/notification_preference_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="Downtime" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_preference_status"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/notification_preference_collapse_barrier"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/notification_preference_chevron"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/notification_preference_image"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/notification_preference_name"
|
||||||
|
tools:text="Off" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/notification_preference_chevron"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:padding="12dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/notification_preference_collapse_barrier"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/circled_chevron" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/notification_preference_collapse_barrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="12dp"
|
||||||
|
app:constraint_referenced_ids="notification_preference_image,notification_preference_status,notification_preference_chevron" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_preference_1hr"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:paddingStart="76dp"
|
||||||
|
android:paddingEnd="56dp"
|
||||||
|
android:text="@string/NotificationProfileSelection__for_1_hour"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/notification_preference_collapse_barrier" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
tools:visibility="gone"
|
||||||
|
android:id="@+id/notification_preference_6pm"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:paddingStart="76dp"
|
||||||
|
android:paddingEnd="56dp"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/notification_preference_1hr"
|
||||||
|
tools:text="Until 6pm" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_preference_view_settings"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:paddingStart="76dp"
|
||||||
|
android:paddingEnd="56dp"
|
||||||
|
android:text="@string/NotificationProfileSelection__view_settings"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/notification_preference_6pm" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/notification_preference_expanded"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:constraint_referenced_ids="notification_preference_1hr,notification_preference_6pm,notification_preference_view_settings" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:background="@drawable/dsl_preference_item_background"
|
||||||
|
android:minHeight="56dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:background="@drawable/tinted_circle"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:backgroundTint="@color/blue_200"
|
||||||
|
tools:srcCompat="@drawable/ic_advanced_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/summary"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/switch_widget"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_goneMarginBottom="16dp"
|
||||||
|
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||||
|
tools:text="Message font size" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:lineSpacingExtra="4sp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:textColor="@color/text_color_secondary_enabled_selector"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/switch_widget"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||||
|
app:layout_goneMarginTop="16dp"
|
||||||
|
tools:text="Some random text to get stuff onto more than one line but not absurdly long like lorem/random"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_widget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:clickable="false"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:background="?selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:minHeight="64dp"
|
||||||
|
android:paddingStart="@dimen/dsl_settings_gutter">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
android:id="@+id/recipient_avatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
|
android:id="@+id/recipient_badge"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginTop="22dp"
|
||||||
|
android:contentDescription="@string/ImageView__badge"
|
||||||
|
app:badge_size="medium"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/recipient_avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/recipient_avatar" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
android:id="@+id/recipient_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/recipient_remove"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||||
|
tools:text="Miles Morales" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/recipient_remove"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
app:srcCompat="@drawable/ic_minus_circle_20" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/notification_profiles_empty_icon"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="88dp"
|
||||||
|
android:layout_marginTop="96dp"
|
||||||
|
android:background="@drawable/tinted_circle"
|
||||||
|
android:padding="20dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_sleeping_face"
|
||||||
|
tools:backgroundTint="#E3E3FE" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_profiles_empty_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/NotificationProfilesFragment__notification_profiles"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profiles_empty_icon" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_profiles_empty_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profiles_empty_title"
|
||||||
|
android:text="@string/NotificationProfilesFragment__create_a_profile_to_receive_notifications_and_calls_only_from_the_people_and_groups_you_want_to_hear_from" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/notification_profiles_empty_create_profile"
|
||||||
|
style="@style/Signal.Widget.Button.Large.Primary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/NotificationProfilesFragment__create_profile"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/notification_profiles_empty_description" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout 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:minHeight="86dp"
|
||||||
|
tools:background="@color/blue">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="44dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/top_toast_popup_background"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:minHeight="44dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/top_toast_popup_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="21dp"
|
||||||
|
android:paddingBottom="1dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/top_toast_popup_description"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@drawable/ic_moon_16" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/top_toast_popup_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="21dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/top_toast_popup_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/first_names" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_edit"
|
||||||
|
android:icon="@drawable/ic_compose_tinted_24"
|
||||||
|
android:title="@string/NotificationProfileDetails__edit_notification_profile"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
</menu>
|
|
@ -16,6 +16,9 @@
|
||||||
<item android:title="@string/text_secure_normal__menu_settings"
|
<item android:title="@string/text_secure_normal__menu_settings"
|
||||||
android:id="@+id/menu_settings" />
|
android:id="@+id/menu_settings" />
|
||||||
|
|
||||||
|
<item android:title="@string/ConversationListFragment__notification_profile"
|
||||||
|
android:id="@+id/menu_notification_profile" />
|
||||||
|
|
||||||
<item android:title="@string/Insights__title"
|
<item android:title="@string/Insights__title"
|
||||||
android:id="@+id/menu_insights"
|
android:id="@+id/menu_insights"
|
||||||
android:visible="false" />
|
android:visible="false" />
|
||||||
|
|
|
@ -284,7 +284,17 @@
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/notificationsSettingsFragment"
|
android:id="@+id/notificationsSettingsFragment"
|
||||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.NotificationsSettingsFragment"
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.NotificationsSettingsFragment"
|
||||||
android:label="notifications_settings_fragment" />
|
android:label="notifications_settings_fragment">
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationsSettingsFragment_to_notificationProfilesFragment"
|
||||||
|
app:destination="@id/notificationProfilesFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<!-- region Privacy -->
|
<!-- region Privacy -->
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -465,6 +475,35 @@
|
||||||
app:exitAnim="@anim/fragment_open_exit"
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
app:popEnterAnim="@anim/fragment_close_enter"
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
app:popExitAnim="@anim/fragment_close_exit" />
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_direct_to_notificationProfiles"
|
||||||
|
app:destination="@id/notificationProfilesFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_direct_to_createNotificationProfiles"
|
||||||
|
app:destination="@id/editNotificationProfileFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit"
|
||||||
|
app:popUpTo="@id/app_settings"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_direct_to_notificationProfileDetails"
|
||||||
|
app:destination="@id/notificationProfileDetailsFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit"
|
||||||
|
app:popUpTo="@id/app_settings"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
<!-- endregion -->
|
<!-- endregion -->
|
||||||
|
|
||||||
<!-- Internal Settings -->
|
<!-- Internal Settings -->
|
||||||
|
@ -570,5 +609,176 @@
|
||||||
|
|
||||||
<include app:graph="@navigation/manage_badges" />
|
<include app:graph="@navigation/manage_badges" />
|
||||||
|
|
||||||
|
<!-- endregion -->
|
||||||
|
|
||||||
|
<!-- Notification Profiles -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/notificationProfilesFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesFragment"
|
||||||
|
tools:layout="@layout/notification_profiles_empty">
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationProfilesFragment_to_editNotificationProfileFragment"
|
||||||
|
app:destination="@id/editNotificationProfileFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationProfilesFragment_to_notificationProfileDetailsFragment"
|
||||||
|
app:destination="@id/notificationProfileDetailsFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/editNotificationProfileFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileFragment"
|
||||||
|
tools:layout="@layout/fragment_edit_notification_profile">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="profileId"
|
||||||
|
android:defaultValue="-1L"
|
||||||
|
app:argType="long" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_editNotificationProfileFragment_to_addAllowedMembersFragment"
|
||||||
|
app:destination="@id/addAllowedMembersFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit"
|
||||||
|
app:popUpTo="@id/editNotificationProfileFragment"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/addAllowedMembersFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersFragment"
|
||||||
|
tools:layout="@layout/fragment_add_allowed_members">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="profileId"
|
||||||
|
app:argType="long" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_addAllowedMembersFragment_to_selectRecipientsFragment"
|
||||||
|
app:destination="@id/selectRecipientsFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_addAllowedMembersFragment_to_editNotificationProfileScheduleFragment"
|
||||||
|
app:destination="@id/editNotificationProfileScheduleFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/selectRecipientsFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.SelectRecipientsFragment"
|
||||||
|
tools:layout="@layout/fragment_select_recipients_fragment">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="profileId"
|
||||||
|
app:argType="long" />
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="currentSelection"
|
||||||
|
android:defaultValue="@null"
|
||||||
|
app:argType="org.thoughtcrime.securesms.recipients.RecipientId[]"
|
||||||
|
app:nullable="true" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/notificationProfileDetailsFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfileDetailsFragment"
|
||||||
|
tools:layout="@layout/dsl_settings_fragment">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="profileId"
|
||||||
|
app:argType="long" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationProfileDetailsFragment_to_selectRecipientsFragment"
|
||||||
|
app:destination="@id/selectRecipientsFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationProfileDetailsFragment_to_editNotificationProfileFragment"
|
||||||
|
app:destination="@id/editNotificationProfileFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationProfileDetailsFragment_to_editNotificationProfileScheduleFragment"
|
||||||
|
app:destination="@id/editNotificationProfileScheduleFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/editNotificationProfileScheduleFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragment"
|
||||||
|
tools:layout="@layout/fragment_edit_notification_profile_schedule">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="profileId"
|
||||||
|
app:argType="long" />
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="createMode"
|
||||||
|
app:argType="boolean" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_editNotificationProfileScheduleFragment_to_notificationProfileCreatedFragment"
|
||||||
|
app:destination="@id/notificationProfileCreatedFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit"
|
||||||
|
app:popUpTo="@id/addAllowedMembersFragment"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/notificationProfileCreatedFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfileCreatedFragment"
|
||||||
|
tools:layout="@layout/fragment_notification_profile_created">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="profileId"
|
||||||
|
app:argType="long" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_notificationProfileCreatedFragment_to_notificationProfileDetailsFragment"
|
||||||
|
app:destination="@id/notificationProfileDetailsFragment"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit"
|
||||||
|
app:popUpTo="@id/notificationProfileCreatedFragment"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
|
</fragment>
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
|
@ -168,4 +168,8 @@
|
||||||
<color name="voice_note_player_speed_background_tint">@color/core_grey_65</color>
|
<color name="voice_note_player_speed_background_tint">@color/core_grey_65</color>
|
||||||
|
|
||||||
<color name="signal_accent_primary_transparent_15">#266191f3</color>
|
<color name="signal_accent_primary_transparent_15">#266191f3</color>
|
||||||
|
|
||||||
|
<color name="notification_profile_moon_tint">#8B8BF9</color>
|
||||||
|
|
||||||
|
<color name="top_toast_background_color">@color/core_grey_80</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -168,4 +168,8 @@
|
||||||
<color name="voice_note_player_speed_background_tint">@color/transparent_black_08</color>
|
<color name="voice_note_player_speed_background_tint">@color/transparent_black_08</color>
|
||||||
|
|
||||||
<color name="signal_accent_primary_transparent_15">#262c6bed</color>
|
<color name="signal_accent_primary_transparent_15">#262c6bed</color>
|
||||||
|
|
||||||
|
<color name="notification_profile_moon_tint">#5151F6</color>
|
||||||
|
|
||||||
|
<color name="top_toast_background_color">@color/core_white</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -140,4 +140,19 @@
|
||||||
<item name="buttonBarNegativeButtonStyle">@style/Signal.Widget.Button.Dialog</item>
|
<item name="buttonBarNegativeButtonStyle">@style/Signal.Widget.Button.Dialog</item>
|
||||||
<item name="textColorAlertDialogListItem">@color/signal_text_secondary</item>
|
<item name="textColorAlertDialogListItem">@color/signal_text_secondary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Signal.Widget.TimePicker" parent="Widget.MaterialComponents.TimePicker">
|
||||||
|
<item name="shapeAppearance">@style/Signal.ShapeOverlay.Rounded</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Signal.Widget.TimePicker.ImageButton" parent="Widget.MaterialComponents.TimePicker.ImageButton">
|
||||||
|
<item name="iconTint">@color/signal_icon_tint_primary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Signal.ThemeOverlay.TimePicker" parent="@style/ThemeOverlay.MaterialComponents.TimePicker">
|
||||||
|
<item name="textAppearanceOverline">@style/TextAppearance.Signal.Body2</item>
|
||||||
|
<item name="colorSurface">@color/signal_background_dialog</item>
|
||||||
|
<item name="borderlessButtonStyle">@style/Signal.Widget.Button.Dialog</item>
|
||||||
|
<item name="colorPrimary">@color/signal_accent_primary</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -345,6 +345,8 @@
|
||||||
<string name="ConversationFragment_not_now">Not now</string>
|
<string name="ConversationFragment_not_now">Not now</string>
|
||||||
<string name="ConversationFragment_your_safety_number_with_s_changed">Your safety number with %s changed</string>
|
<string name="ConversationFragment_your_safety_number_with_s_changed">Your safety number with %s changed</string>
|
||||||
<string name="ConversationFragment_your_safety_number_with_s_changed_likey_because_they_reinstalled_signal">Your safety number with %s changed, likely because they reinstalled Signal or changed devices. Tap Verify to confirm the new safety number. This is optional.</string>
|
<string name="ConversationFragment_your_safety_number_with_s_changed_likey_because_they_reinstalled_signal">Your safety number with %s changed, likely because they reinstalled Signal or changed devices. Tap Verify to confirm the new safety number. This is optional.</string>
|
||||||
|
<!-- Message shown to indicate which notification profile is on/active -->
|
||||||
|
<string name="ConversationFragment__s_on">%1$s on</string>
|
||||||
|
|
||||||
<plurals name="ConversationListFragment_delete_selected_conversations">
|
<plurals name="ConversationListFragment_delete_selected_conversations">
|
||||||
<item quantity="one">Delete selected conversation?</item>
|
<item quantity="one">Delete selected conversation?</item>
|
||||||
|
@ -408,6 +410,13 @@
|
||||||
<item quantity="other">%d selected</item>
|
<item quantity="other">%d selected</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Show in conversation list overflow menu to open selection bottom sheet -->
|
||||||
|
<string name="ConversationListFragment__notification_profile">Notification Profile</string>
|
||||||
|
<!-- Tooltip shown after you have created your first notification profile -->
|
||||||
|
<string name="ConversationListFragment__turn_your_notification_profile_on_or_off_here">Turn your notification profile on or off here.</string>
|
||||||
|
<!-- Message shown in top toast to indicate the named profile is on -->
|
||||||
|
<string name="ConversationListFragment__s_on">%1$s on</string>
|
||||||
|
|
||||||
<!-- ConversationListItem -->
|
<!-- ConversationListItem -->
|
||||||
<string name="ConversationListItem_key_exchange_message">Key exchange message</string>
|
<string name="ConversationListItem_key_exchange_message">Key exchange message</string>
|
||||||
|
|
||||||
|
@ -2664,7 +2673,7 @@
|
||||||
<string name="PaymentsAddMoneyFragment__copied_to_clipboard">Copied to clipboard</string>
|
<string name="PaymentsAddMoneyFragment__copied_to_clipboard">Copied to clipboard</string>
|
||||||
<string name="PaymentsAddMoneyFragment__to_add_funds">To add funds, send MobileCoin to your wallet address. Start a transaction from your account on an exchange that supports MobileCoin, then scan the QR code or copy your wallet address.</string>
|
<string name="PaymentsAddMoneyFragment__to_add_funds">To add funds, send MobileCoin to your wallet address. Start a transaction from your account on an exchange that supports MobileCoin, then scan the QR code or copy your wallet address.</string>
|
||||||
<string name="PaymentsAddMoneyFragment__learn_more__information" translatable="false">https://support.signal.org/hc/articles/360057625692#payments_transfer_from_exchange</string>
|
<string name="PaymentsAddMoneyFragment__learn_more__information" translatable="false">https://support.signal.org/hc/articles/360057625692#payments_transfer_from_exchange</string>
|
||||||
|
|
||||||
<!-- PaymentsDetailsFragment -->
|
<!-- PaymentsDetailsFragment -->
|
||||||
<string name="PaymentsDetailsFragment__details">Details</string>
|
<string name="PaymentsDetailsFragment__details">Details</string>
|
||||||
<string name="PaymentsDetailsFragment__status">Status</string>
|
<string name="PaymentsDetailsFragment__status">Status</string>
|
||||||
|
@ -3640,6 +3649,18 @@
|
||||||
<string name="NotificationsSettingsFragment__calls">Calls</string>
|
<string name="NotificationsSettingsFragment__calls">Calls</string>
|
||||||
<string name="NotificationsSettingsFragment__notify_when">Notify when…</string>
|
<string name="NotificationsSettingsFragment__notify_when">Notify when…</string>
|
||||||
<string name="NotificationsSettingsFragment__contact_joins_signal">Contact joins Signal</string>
|
<string name="NotificationsSettingsFragment__contact_joins_signal">Contact joins Signal</string>
|
||||||
|
<!-- Notification preference header -->
|
||||||
|
<string name="NotificationsSettingsFragment__notification_profiles">Notification profiles</string>
|
||||||
|
<!-- Notification preference option header -->
|
||||||
|
<string name="NotificationsSettingsFragment__profiles">Profiles</string>
|
||||||
|
<!-- Notification preference summary text -->
|
||||||
|
<string name="NotificationsSettingsFragment__set_up_notification_profiles">Set up notification profiles</string>
|
||||||
|
|
||||||
|
<!-- NotificationProfilesFragment -->
|
||||||
|
<!-- Title for notification profiles screen that shows all existing profiles -->
|
||||||
|
<string name="NotificationProfilesFragment__notification_profiles">Notification profiles</string>
|
||||||
|
<!-- Button text to create a notification profile -->
|
||||||
|
<string name="NotificationProfilesFragment__create_profile">Create profile</string>
|
||||||
|
|
||||||
<!-- PrivacySettingsFragment -->
|
<!-- PrivacySettingsFragment -->
|
||||||
<string name="PrivacySettingsFragment__blocked">Blocked</string>
|
<string name="PrivacySettingsFragment__blocked">Blocked</string>
|
||||||
|
@ -3933,7 +3954,7 @@
|
||||||
<string name="MediaReviewImagePageFragment__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
|
<string name="MediaReviewImagePageFragment__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
|
||||||
|
|
||||||
<string name="CameraFragment__failed_to_open_camera">Failed to open camera</string>
|
<string name="CameraFragment__failed_to_open_camera">Failed to open camera</string>
|
||||||
|
|
||||||
<string name="BadgesOverviewFragment__my_badges">My badges</string>
|
<string name="BadgesOverviewFragment__my_badges">My badges</string>
|
||||||
<string name="BadgesOverviewFragment__featured_badge">Featured badge</string>
|
<string name="BadgesOverviewFragment__featured_badge">Featured badge</string>
|
||||||
<string name="BadgesOverviewFragment__display_badges_on_profile">Display badges on profile</string>
|
<string name="BadgesOverviewFragment__display_badges_on_profile">Display badges on profile</string>
|
||||||
|
@ -4049,6 +4070,149 @@
|
||||||
|
|
||||||
<string name="Boost__thank_you_for_your_donation" translatable="false">Thank you for your donation. Your contribution helps fuel the mission of developing open source privacy technology that protects free expression and enables secure global communication for millions around the world. Signal Technology Foundation is a tax-exempt nonprofit organization in the United States under section 501c3 of the Internal Revenue Code. Our Federal Tax ID is 82-4506840. No goods or services were provided in exchange for this donation. Please retain this receipt for your tax records.</string>
|
<string name="Boost__thank_you_for_your_donation" translatable="false">Thank you for your donation. Your contribution helps fuel the mission of developing open source privacy technology that protects free expression and enables secure global communication for millions around the world. Signal Technology Foundation is a tax-exempt nonprofit organization in the United States under section 501c3 of the Internal Revenue Code. Our Federal Tax ID is 82-4506840. No goods or services were provided in exchange for this donation. Please retain this receipt for your tax records.</string>
|
||||||
|
|
||||||
|
<!-- Title of create notification profile screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__name_your_profile">Name your profile</string>
|
||||||
|
<!-- Hint text for create/edit notification profile name -->
|
||||||
|
<string name="EditNotificationProfileFragment__profile_name">Profile name</string>
|
||||||
|
<!-- Name has a max length, this shows how many characters are used out of the max -->
|
||||||
|
<string name="EditNotificationProfileFragment__count">%1$d/%2$d</string>
|
||||||
|
<!-- Call to action button once the profile is named to continue to the next create step -->
|
||||||
|
<string name="EditNotificationProfileFragment__next">Next</string>
|
||||||
|
<!-- Call to action button once the profile name is edited -->
|
||||||
|
<string name="EditNotificationProfileFragment__save">Save</string>
|
||||||
|
<!-- Title of edit notification profile screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__edit_this_profile">Edit this profile</string>
|
||||||
|
<!-- Error message shown when attempting to create or edit a profile name to an existing profile name -->
|
||||||
|
<string name="EditNotificationProfileFragment__a_profile_with_this_name_already_exists">A profile with this name already exists</string>
|
||||||
|
<!-- Preset selectable name for a profile name, shown as list in edit/create screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__work">Work</string>
|
||||||
|
<!-- Preset selectable name for a profile name, shown as list in edit/create screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__sleep">Sleep</string>
|
||||||
|
<!-- Preset selectable name for a profile name, shown as list in edit/create screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__driving">Driving</string>
|
||||||
|
<!-- Preset selectable name for a profile name, shown as list in edit/create screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__downtime">Downtime</string>
|
||||||
|
<!-- Preset selectable name for a profile name, shown as list in edit/create screen -->
|
||||||
|
<string name="EditNotificationProfileFragment__focus">Focus</string>
|
||||||
|
<!-- Error message shown when attempting to next/save without a profile name -->
|
||||||
|
<string name="EditNotificationProfileFragment__profile_must_have_a_name">Must have a name</string>
|
||||||
|
|
||||||
|
<!-- Title for add recipients to notification profile screen in create flow -->
|
||||||
|
<string name="AddAllowedMembers__allowed_notifications">Allowed notifications</string>
|
||||||
|
<!-- Description of what the user should be doing with this screen -->
|
||||||
|
<string name="AddAllowedMembers__add_people_and_groups_you_want_notifications_and_calls_from_when_this_profile_is_on">Add people and groups you want notifications and calls from when this profile is on</string>
|
||||||
|
<!-- Button text that launches the contact picker to select from -->
|
||||||
|
<string name="AddAllowedMembers__add_people_or_groups">Add people or groups</string>
|
||||||
|
|
||||||
|
<!-- Call to action button on contact picker for adding to profile -->
|
||||||
|
<string name="SelectRecipientsFragment__add">Add</string>
|
||||||
|
|
||||||
|
<!-- Notification profiles home fragment, shown when no profiles have been created yet -->
|
||||||
|
<string name="NotificationProfilesFragment__create_a_profile_to_receive_notifications_and_calls_only_from_the_people_and_groups_you_want_to_hear_from">Create a profile to receive notifications and calls only from the people and groups you want to hear from.</string>
|
||||||
|
<!-- Header shown above list of all notification profiles -->
|
||||||
|
<string name="NotificationProfilesFragment__profiles">Profiles</string>
|
||||||
|
<!-- Button that starts the create new notification profile flow -->
|
||||||
|
<string name="NotificationProfilesFragment__new_profile">New profile</string>
|
||||||
|
<!-- Profile active status, indicating the current profile is on for an unknown amount of time -->
|
||||||
|
<string name="NotificationProfilesFragment__on">On</string>
|
||||||
|
|
||||||
|
<!-- Button use to permanently delete a notification profile -->
|
||||||
|
<string name="NotificationProfileDetails__delete_profile">Delete profile</string>
|
||||||
|
<!-- Snakbar message shown when removing a recipient from a profile -->
|
||||||
|
<string name="NotificationProfileDetails__s_removed">\"%1$s\" removed.</string>
|
||||||
|
<!-- Snackbar button text that will undo the recipient remove -->
|
||||||
|
<string name="NotificationProfileDetails__undo">Undo</string>
|
||||||
|
<!-- Dialog message shown to confirm deleting a profile -->
|
||||||
|
<string name="NotificationProfileDetails__permanently_delete_profile">Permanently delete profile?</string>
|
||||||
|
<!-- Dialog button to delete profile -->
|
||||||
|
<string name="NotificationProfileDetails__delete">Delete</string>
|
||||||
|
<!-- Title/accessibility text for edit icon to edit profile emoji/name -->
|
||||||
|
<string name="NotificationProfileDetails__edit_notification_profile">Edit notification profile</string>
|
||||||
|
<!-- Schedule description if all days are selected -->
|
||||||
|
<string name="NotificationProfileDetails__everyday">Everyday</string>
|
||||||
|
<!-- Profile status on if it is the active profile -->
|
||||||
|
<string name="NotificationProfileDetails__on">On</string>
|
||||||
|
<!-- Profile status on if it is not the active profile -->
|
||||||
|
<string name="NotificationProfileDetails__off">Off</string>
|
||||||
|
<!-- Description of hours for schedule (start to end) times -->
|
||||||
|
<string name="NotificationProfileDetails__s_to_s">%1$s to %2$s</string>
|
||||||
|
<!-- Section header for exceptions to the notification profile -->
|
||||||
|
<string name="NotificationProfileDetails__exceptions">Exceptions</string>
|
||||||
|
<!-- Profile exception to allow all calls through the profile restrictions -->
|
||||||
|
<string name="NotificationProfileDetails__allow_all_calls">Allow all calls</string>
|
||||||
|
<!-- Profile exception to allow all @mentions through the profile restrictions -->
|
||||||
|
<string name="NotificationProfileDetails__notify_for_all_mentions">Notify for all mentions</string>
|
||||||
|
<!-- Section header for showing schedule information -->
|
||||||
|
<string name="NotificationProfileDetails__schedule">Schedule</string>
|
||||||
|
|
||||||
|
<!-- Title for add schedule to profile in create flow -->
|
||||||
|
<string name="EditNotificationProfileSchedule__add_a_schedule">Add a schedule</string>
|
||||||
|
<!-- Descriptor text indicating what the user can do with this screen -->
|
||||||
|
<string name="EditNotificationProfileSchedule__turn_on_and_edit_your_schedule_to_automate_this_profile">Turn on and edit your schedule to automate this profile.</string>
|
||||||
|
<!-- Text shown next to toggle switch to enable/disable schedule -->
|
||||||
|
<string name="EditNotificationProfileSchedule__schedule">Schedule</string>
|
||||||
|
<!-- Label for showing the start time for the schedule -->
|
||||||
|
<string name="EditNotificationProfileSchedule__start">Start</string>
|
||||||
|
<!-- Label for showing the end time for the schedule -->
|
||||||
|
<string name="EditNotificationProfileSchedule__end">End</string>
|
||||||
|
<!-- First letter of Sunday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__sunday_first_letter">S</string>
|
||||||
|
<!-- First letter of Monday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__monday_first_letter">M</string>
|
||||||
|
<!-- First letter of Tuesday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__tuesday_first_letter">T</string>
|
||||||
|
<!-- First letter of Wednesday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__wednesday_first_letter">W</string>
|
||||||
|
<!-- First letter of Thursday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__thursday_first_letter">T</string>
|
||||||
|
<!-- First letter of Friday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__friday_first_letter">F</string>
|
||||||
|
<!-- First letter of Saturday -->
|
||||||
|
<string name="EditNotificationProfileSchedule__saturday_first_letter">S</string>
|
||||||
|
<!-- Title of select time dialog shown when setting start time for schedule -->
|
||||||
|
<string name="EditNotificationProfileSchedule__set_start_time">Set start time</string>
|
||||||
|
<!-- Title of select time dialog shown when setting end time for schedule -->
|
||||||
|
<string name="EditNotificationProfileSchedule__set_end_time">Set end time</string>
|
||||||
|
<!-- If in edit mode, call to action button text show to save schedule to profile -->
|
||||||
|
<string name="EditNotificationProfileSchedule__save">Save</string>
|
||||||
|
<!-- If in create mode, call to action button text to show to skip enabling a schedule -->
|
||||||
|
<string name="EditNotificationProfileSchedule__skip">Skip</string>
|
||||||
|
<!-- If in create mode, call to action button text to show to use the enabled schedule and move to the next screen -->
|
||||||
|
<string name="EditNotificationProfileSchedule__next">Next</string>
|
||||||
|
<!-- Error message shown if trying to save/use a schedule with no days selected -->
|
||||||
|
<string name="EditNotificationProfileSchedule__schedule_must_have_at_least_one_day">Schedule must have at least one day</string>
|
||||||
|
|
||||||
|
<!-- Title for final screen shown after completing a profile creation -->
|
||||||
|
<string name="NotificationProfileCreated__profile_created">Profile created</string>
|
||||||
|
<!-- Call to action button to press to close the created screen and move to the profile details screen -->
|
||||||
|
<string name="NotificationProfileCreated__done">Done</string>
|
||||||
|
<!-- Descriptor text shown to indicate how to manually turn a profile on/off -->
|
||||||
|
<string name="NotificationProfileCreated__you_can_turn_your_profile_on_or_off_manually_via_the_menu_on_the_chat_list">You can turn your profile on or off manually via the menu on the chat list.</string>
|
||||||
|
<!-- Descriptor text shown to indicate you can add a schedule later since you did not add one during create flow -->
|
||||||
|
<string name="NotificationProfileCreated__add_a_schedule_in_settings_to_automate_your_profile">Add a schedule in settings to automate your profile.</string>
|
||||||
|
<!-- Descriptor text shown to indicate your profile will follow the schedule set during create flow -->
|
||||||
|
<string name="NotificationProfileCreated__your_profile_will_turn_on_and_off_automatically_according_to_your_schedule">Your profile will turn on and off automatically according to your schedule.</string>
|
||||||
|
|
||||||
|
<!-- Button text shown in profile selection bottom sheet to create a new profile -->
|
||||||
|
<string name="NotificationProfileSelection__new_notification_profile">New notification profile</string>
|
||||||
|
<!-- Manual enable option to manually enable a profile for 1 hour -->
|
||||||
|
<string name="NotificationProfileSelection__for_1_hour">For 1 Hour</string>
|
||||||
|
<!-- Manual enable option to manually enable a profile until a set time (currently 6pm or 8am depending on what is next) -->
|
||||||
|
<string name="NotificationProfileSelection__until_s">Until %1$s</string>
|
||||||
|
<!-- Option to view profile details -->
|
||||||
|
<string name="NotificationProfileSelection__view_settings">View settings</string>
|
||||||
|
<!-- Descriptor text indicating how long a profile will be on when there is a time component associated with it -->
|
||||||
|
<string name="NotificationProfileSelection__on_until_s">On until %1$s</string>
|
||||||
|
|
||||||
|
<!-- Title for notification profile megaphone -->
|
||||||
|
<string name="NotificationProfilesMegaphone__notification_profiles">Notification profiles</string>
|
||||||
|
<!-- Description for notification profile megaphone -->
|
||||||
|
<string name="NotificationProfilesMegaphone__only_get_notifications_from_the_people_and_groups_you_choose">Only get notifications from the people and groups you choose.</string>
|
||||||
|
<!-- Call to action button to create a profile from megaphone -->
|
||||||
|
<string name="NotificationProfilesMegaphone__create_a_profile">Create a profile</string>
|
||||||
|
<!-- Button to dismiss notification profile megaphone -->
|
||||||
|
<string name="NotificationProfilesMegaphone__not_now">Now now</string>
|
||||||
|
|
||||||
<!-- Displayed in a toast when we fail to open the ringtone picker -->
|
<!-- Displayed in a toast when we fail to open the ringtone picker -->
|
||||||
<string name="NotificationSettingsFragment__failed_to_open_picker">Failed to open picker.</string>
|
<string name="NotificationSettingsFragment__failed_to_open_picker">Failed to open picker.</string>
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,8 @@
|
||||||
<item name="colorControlNormal">@color/core_grey_90</item>
|
<item name="colorControlNormal">@color/core_grey_90</item>
|
||||||
|
|
||||||
<item name="materialAlertDialogTheme">@style/Signal.ThemeOverlay.Dialog.Rounded</item>
|
<item name="materialAlertDialogTheme">@style/Signal.ThemeOverlay.Dialog.Rounded</item>
|
||||||
|
<item name="materialTimePickerTheme">@style/Signal.ThemeOverlay.TimePicker</item>
|
||||||
|
<item name="materialTimePickerStyle">@style/Signal.Widget.TimePicker</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TextSecure.DarkTheme" parent="@style/TextSecure.BaseDarkTheme">
|
<style name="TextSecure.DarkTheme" parent="@style/TextSecure.BaseDarkTheme">
|
||||||
|
@ -242,6 +244,8 @@
|
||||||
<item name="colorControlNormal">@color/core_white</item>
|
<item name="colorControlNormal">@color/core_white</item>
|
||||||
|
|
||||||
<item name="materialAlertDialogTheme">@style/Signal.ThemeOverlay.Dialog.Rounded</item>
|
<item name="materialAlertDialogTheme">@style/Signal.ThemeOverlay.Dialog.Rounded</item>
|
||||||
|
<item name="materialTimePickerTheme">@style/Signal.ThemeOverlay.TimePicker</item>
|
||||||
|
<item name="materialTimePickerStyle">@style/Signal.Widget.TimePicker</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Signal.AlertDialog.Light.Cornered" parent="Theme.AppCompat.Light.Dialog.Alert">
|
<style name="Theme.Signal.AlertDialog.Light.Cornered" parent="Theme.AppCompat.Light.Dialog.Alert">
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import org.junit.runners.model.Statement
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.dependencies.MockApplicationDependencyProvider
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule to setup [SignalStore] with a mock [KeyValueDataSet]. Must be used with Roboelectric.
|
||||||
|
*
|
||||||
|
* Can provide [defaultValues] to set the same values before each test and use [dataSet] directly to add any
|
||||||
|
* test specific values.
|
||||||
|
*
|
||||||
|
* The [dataSet] is reset at the beginning of each test to an empty state.
|
||||||
|
*/
|
||||||
|
class SignalStoreRule @JvmOverloads constructor(private val defaultValues: KeyValueDataSet.() -> Unit = {}) : TestRule {
|
||||||
|
var dataSet = KeyValueDataSet()
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun apply(base: Statement, description: Description): Statement {
|
||||||
|
return object : Statement() {
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
override fun evaluate() {
|
||||||
|
if (!ApplicationDependencies.isInitialized()) {
|
||||||
|
ApplicationDependencies.init(ApplicationProvider.getApplicationContext(), MockApplicationDependencyProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSet = KeyValueDataSet()
|
||||||
|
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
|
||||||
|
defaultValues.invoke(dataSet)
|
||||||
|
|
||||||
|
base.evaluate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers.containsInAnyOrder
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.dependencies.MockApplicationDependencyProvider
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(manifest = Config.NONE, application = Application::class)
|
||||||
|
class NotificationProfileDatabaseTest {
|
||||||
|
|
||||||
|
private lateinit var db: SQLiteDatabase
|
||||||
|
private lateinit var database: NotificationProfileDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
|
||||||
|
NotificationProfileDatabase.CREATE_TABLE.forEach { println(it); this.execSQL(it) }
|
||||||
|
NotificationProfileDatabase.CREATE_INDEXES.forEach { println(it); this.execSQL(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ApplicationDependencies.isInitialized()) {
|
||||||
|
ApplicationDependencies.init(ApplicationProvider.getApplicationContext(), MockApplicationDependencyProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
db = sqlCipher.writableDatabase
|
||||||
|
database = NotificationProfileDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `addProfile for profile with empty schedule and members`() {
|
||||||
|
val profile: NotificationProfile = database.createProfile(
|
||||||
|
name = "Profile",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
).profile
|
||||||
|
|
||||||
|
assertEquals(1, profile.id)
|
||||||
|
assertEquals("Profile", profile.name)
|
||||||
|
assertEquals("avatar", profile.emoji)
|
||||||
|
assertEquals(1000L, profile.createdAt)
|
||||||
|
assertEquals(1, profile.schedule.id)
|
||||||
|
|
||||||
|
val profiles = database.getProfiles()
|
||||||
|
|
||||||
|
assertEquals(1, profiles.size)
|
||||||
|
assertEquals(1, profiles[0].id)
|
||||||
|
assertEquals("Profile", profiles[0].name)
|
||||||
|
assertEquals("avatar", profiles[0].emoji)
|
||||||
|
assertEquals(1000L, profiles[0].createdAt)
|
||||||
|
assertEquals(1, profiles[0].schedule.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updateProfile changes all updateable fields`() {
|
||||||
|
val profile: NotificationProfile = database.createProfile(
|
||||||
|
name = "Profile",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
).profile
|
||||||
|
|
||||||
|
val updatedProfile = database.updateProfile(
|
||||||
|
profile.copy(
|
||||||
|
name = "Profile 2",
|
||||||
|
emoji = "avatar 2",
|
||||||
|
allowAllCalls = true,
|
||||||
|
allowAllMentions = true
|
||||||
|
)
|
||||||
|
).profile
|
||||||
|
|
||||||
|
assertEquals("Profile 2", updatedProfile.name)
|
||||||
|
assertEquals("avatar 2", updatedProfile.emoji)
|
||||||
|
assertEquals(1000L, updatedProfile.createdAt)
|
||||||
|
assertTrue(updatedProfile.allowAllCalls)
|
||||||
|
assertTrue(updatedProfile.allowAllMentions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when allowed recipients change profile changes`() {
|
||||||
|
val profile: NotificationProfile = database.createProfile(
|
||||||
|
name = "Profile",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
).profile
|
||||||
|
assertFalse(profile.isRecipientAllowed(RecipientId.from(1)))
|
||||||
|
|
||||||
|
var updated = database.addAllowedRecipient(profile.id, RecipientId.from(1))
|
||||||
|
assertTrue(updated.isRecipientAllowed(RecipientId.from(1)))
|
||||||
|
|
||||||
|
updated = database.removeAllowedRecipient(profile.id, RecipientId.from(1))
|
||||||
|
assertFalse(updated.isRecipientAllowed(RecipientId.from(1)))
|
||||||
|
|
||||||
|
updated = database.updateProfile(updated.copy(allowedMembers = setOf(RecipientId.from(1)))).profile
|
||||||
|
assertTrue(updated.isRecipientAllowed(RecipientId.from(1)))
|
||||||
|
|
||||||
|
updated = database.updateProfile(updated.copy(allowedMembers = emptySet())).profile
|
||||||
|
assertFalse(updated.isRecipientAllowed(RecipientId.from(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when schedule change profile changes`() {
|
||||||
|
val profile: NotificationProfile = database.createProfile(
|
||||||
|
name = "Profile",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
).profile
|
||||||
|
assertFalse(profile.schedule.enabled)
|
||||||
|
assertEquals(900, profile.schedule.start)
|
||||||
|
assertEquals(1700, profile.schedule.end)
|
||||||
|
assertThat("Contains correct default days", profile.schedule.daysEnabled, containsInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY))
|
||||||
|
|
||||||
|
database.updateSchedule(profile.schedule.copy(enabled = true, start = 800, end = 1800, daysEnabled = setOf(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY)))
|
||||||
|
var updated = database.getProfile(profile.id)!!
|
||||||
|
assertTrue(updated.schedule.enabled)
|
||||||
|
assertEquals(800, updated.schedule.start)
|
||||||
|
assertEquals(1800, updated.schedule.end)
|
||||||
|
assertThat("Contains updated days days", updated.schedule.daysEnabled, containsInAnyOrder(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY))
|
||||||
|
|
||||||
|
database.updateSchedule(profile.schedule)
|
||||||
|
updated = database.getProfile(profile.id)!!
|
||||||
|
assertFalse(updated.schedule.enabled)
|
||||||
|
assertEquals(900, updated.schedule.start)
|
||||||
|
assertEquals(1700, updated.schedule.end)
|
||||||
|
assertThat("Contains correct default days", updated.schedule.daysEnabled, containsInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY))
|
||||||
|
|
||||||
|
updated = database.updateProfile(profile.copy(schedule = profile.schedule.copy(enabled = true, start = 800, end = 1800, daysEnabled = setOf(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY)))).profile
|
||||||
|
assertTrue(updated.schedule.enabled)
|
||||||
|
assertEquals(800, updated.schedule.start)
|
||||||
|
assertEquals(1800, updated.schedule.end)
|
||||||
|
assertThat("Contains updated days days", updated.schedule.daysEnabled, containsInAnyOrder(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY))
|
||||||
|
|
||||||
|
updated = database.updateProfile(profile).profile
|
||||||
|
assertFalse(updated.schedule.enabled)
|
||||||
|
assertEquals(900, updated.schedule.start)
|
||||||
|
assertEquals(1700, updated.schedule.end)
|
||||||
|
assertThat("Contains correct default days", updated.schedule.daysEnabled, containsInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val NotificationProfileDatabase.NotificationProfileChangeResult.profile: NotificationProfile
|
||||||
|
get() = (this as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue