mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Reimplement megaphone UI in compose.
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class BasicMegaphoneView extends FrameLayout {
|
||||
|
||||
private LottieAnimationView image;
|
||||
private TextView titleText;
|
||||
private TextView bodyText;
|
||||
private Button actionButton;
|
||||
private Button secondaryButton;
|
||||
|
||||
private Megaphone megaphone;
|
||||
private MegaphoneActionController megaphoneListener;
|
||||
|
||||
public BasicMegaphoneView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public BasicMegaphoneView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(@NonNull Context context) {
|
||||
inflate(context, R.layout.basic_megaphone_view, this);
|
||||
|
||||
this.image = findViewById(R.id.basic_megaphone_image);
|
||||
this.titleText = findViewById(R.id.basic_megaphone_title);
|
||||
this.bodyText = findViewById(R.id.basic_megaphone_body);
|
||||
this.actionButton = findViewById(R.id.basic_megaphone_action);
|
||||
this.secondaryButton = findViewById(R.id.basic_megaphone_secondary);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController megaphoneListener) {
|
||||
this.megaphone = megaphone;
|
||||
this.megaphoneListener = megaphoneListener;
|
||||
|
||||
if (megaphone.getImageRes() != 0) {
|
||||
image.setVisibility(VISIBLE);
|
||||
image.setImageResource(megaphone.getImageRes());
|
||||
} else if (megaphone.getImageRequestBuilder() != null) {
|
||||
image.setVisibility(VISIBLE);
|
||||
megaphone.getImageRequestBuilder().into(image);
|
||||
} else if (megaphone.getLottieRes() != 0) {
|
||||
image.setVisibility(VISIBLE);
|
||||
image.setAnimation(megaphone.getLottieRes());
|
||||
image.playAnimation();
|
||||
} else {
|
||||
image.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getTitle().hasText()) {
|
||||
titleText.setVisibility(VISIBLE);
|
||||
titleText.setText(megaphone.getTitle().resolve(getContext()));
|
||||
} else {
|
||||
titleText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getBody().hasText()) {
|
||||
bodyText.setVisibility(VISIBLE);
|
||||
bodyText.setText(megaphone.getBody().resolve(getContext()));
|
||||
} else {
|
||||
bodyText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.hasButton()) {
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setText(megaphone.getButtonText().resolve(getContext()));
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (megaphone.getButtonClickListener() != null) {
|
||||
megaphone.getButtonClickListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.canSnooze() || megaphone.hasSecondaryButton()) {
|
||||
secondaryButton.setVisibility(VISIBLE);
|
||||
|
||||
if (megaphone.canSnooze()) {
|
||||
secondaryButton.setOnClickListener(v -> {
|
||||
megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
|
||||
|
||||
if (megaphone.getSnoozeListener() != null) {
|
||||
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
secondaryButton.setText(megaphone.getSecondaryButtonText().resolve(getContext()));
|
||||
secondaryButton.setOnClickListener(v -> {
|
||||
if (megaphone.getSecondaryButtonClickListener() != null) {
|
||||
megaphone.getSecondaryButtonClickListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
secondaryButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.megaphone
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.IconButtons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.Emojifier
|
||||
import org.thoughtcrime.securesms.main.EmptyMegaphoneActionController
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Allows us to utilize our composeView from Java code.
|
||||
*/
|
||||
fun setContent(composeView: ComposeView, megaphone: Megaphone, megaphoneActionController: MegaphoneActionController) {
|
||||
composeView.setContent {
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(composeView.context)) {
|
||||
MegaphoneComponent(
|
||||
megaphone,
|
||||
megaphoneActionController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable which replaces the whole builder pattern for megaphone views.
|
||||
*/
|
||||
@Composable
|
||||
fun MegaphoneComponent(
|
||||
megaphone: Megaphone,
|
||||
megaphoneActionController: MegaphoneActionController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (megaphone == Megaphone.NONE) {
|
||||
return
|
||||
}
|
||||
|
||||
when (megaphone.style) {
|
||||
Megaphone.Style.ONBOARDING -> OnboardingMegaphone(
|
||||
megaphoneActionController = megaphoneActionController,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Megaphone.Style.BASIC -> BasicMegaphone(
|
||||
megaphone = megaphone,
|
||||
megaphoneActionController = megaphoneActionController,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Megaphone.Style.FULLSCREEN -> Unit
|
||||
Megaphone.Style.POPUP -> PopupMegaphone(
|
||||
megaphone = megaphone,
|
||||
megaphoneActionController = megaphoneActionController,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(megaphone) {
|
||||
megaphone.onVisibleListener?.onEvent(megaphone, megaphoneActionController)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic megaphone with up to two actions, no elevation, and an outline.
|
||||
*/
|
||||
@Composable
|
||||
private fun BasicMegaphone(
|
||||
megaphone: Megaphone,
|
||||
megaphoneActionController: MegaphoneActionController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(
|
||||
elevation = CardDefaults.outlinedCardElevation(),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.38f)),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.then(modifier)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(top = 16.dp, bottom = 8.dp)
|
||||
) {
|
||||
MegaphoneCardContent(megaphone = megaphone)
|
||||
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (megaphone.hasSecondaryButton()) {
|
||||
TextButton(
|
||||
onClick = { megaphone.secondaryButtonClickListener?.onEvent(megaphone, megaphoneActionController) }
|
||||
) {
|
||||
Text(
|
||||
text = megaphone.secondaryButtonText?.resolve(context)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (megaphone.canSnooze()) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
megaphoneActionController.onMegaphoneSnooze(megaphone.event)
|
||||
megaphone.snoozeListener?.onEvent(megaphone, megaphoneActionController)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = megaphone.buttonText?.resolve(context)!!
|
||||
)
|
||||
}
|
||||
} else if (megaphone.hasButton()) {
|
||||
TextButton(
|
||||
onClick = { megaphone.buttonClickListener?.onEvent(megaphone, megaphoneActionController) }
|
||||
) {
|
||||
Text(
|
||||
text = megaphone.buttonText?.resolve(context)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elevated megaphone with no actions but does have a close button.
|
||||
*/
|
||||
@Composable
|
||||
private fun PopupMegaphone(
|
||||
megaphone: Megaphone,
|
||||
megaphoneActionController: MegaphoneActionController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
elevation = CardDefaults.cardElevation(6.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = colorResource(R.color.megaphone_background_color)),
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.then(modifier)
|
||||
) {
|
||||
Box {
|
||||
MegaphoneCardContent(
|
||||
megaphone = megaphone,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
|
||||
IconButtons.IconButton(
|
||||
onClick = {
|
||||
if (megaphone.hasButton()) {
|
||||
megaphone.buttonClickListener?.onEvent(megaphone, megaphoneActionController)
|
||||
} else {
|
||||
megaphoneActionController.onMegaphoneCompleted(megaphone.event)
|
||||
}
|
||||
},
|
||||
size = 48.dp,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_x_20),
|
||||
contentDescription = stringResource(R.string.Material3SearchToolbar__close)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared card content including the image, title, and body.
|
||||
*/
|
||||
@Composable
|
||||
private fun MegaphoneCardContent(
|
||||
megaphone: Megaphone,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
MegaphoneImage(
|
||||
megaphone = megaphone,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 8.dp)
|
||||
) {
|
||||
if (megaphone.title.hasText) {
|
||||
Emojifier(
|
||||
text = megaphone.title.resolve(LocalContext.current)!!
|
||||
) { annotatedText, inlineContent ->
|
||||
Text(
|
||||
text = annotatedText,
|
||||
inlineContent = inlineContent,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (megaphone.body.hasText) {
|
||||
Emojifier(
|
||||
text = megaphone.body.resolve(LocalContext.current)!!
|
||||
) { annotatedText, inlineContent ->
|
||||
Text(
|
||||
text = annotatedText,
|
||||
inlineContent = inlineContent,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An image, which is either backed by Lottie, a glide request or just a plain vector.
|
||||
*/
|
||||
@Composable
|
||||
private fun MegaphoneImage(
|
||||
megaphone: Megaphone,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val sharedModifier = modifier.size(64.dp)
|
||||
|
||||
if (megaphone.imageRes != 0) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(megaphone.imageRes),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = sharedModifier
|
||||
)
|
||||
} else if (megaphone.imageRequestBuilder != null) {
|
||||
var drawable: Drawable? by remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
||||
val painter = rememberDrawablePainter(drawable)
|
||||
val size = with(LocalDensity.current) {
|
||||
64.dp.toPx().roundToInt()
|
||||
}
|
||||
|
||||
LaunchedEffect(megaphone.imageRequestBuilder) {
|
||||
drawable = withContext(Dispatchers.IO) {
|
||||
megaphone.imageRequestBuilder?.submit(size, size)?.get()
|
||||
}
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = sharedModifier
|
||||
)
|
||||
} else if (megaphone.lottieRes != 0) {
|
||||
val lottieComposition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(megaphone.lottieRes))
|
||||
|
||||
LottieAnimation(
|
||||
composition = lottieComposition,
|
||||
modifier = sharedModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BasicMegaphonePreview() {
|
||||
Previews.Preview {
|
||||
MegaphoneComponent(
|
||||
megaphone = rememberTestMegaphone(Event.PINS_FOR_ALL, Megaphone.Style.BASIC),
|
||||
megaphoneActionController = EmptyMegaphoneActionController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun PopupMegaphonePreview() {
|
||||
Previews.Preview {
|
||||
MegaphoneComponent(
|
||||
megaphone = rememberTestMegaphone(Event.PIN_REMINDER, Megaphone.Style.POPUP),
|
||||
megaphoneActionController = EmptyMegaphoneActionController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing only.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberTestMegaphone(
|
||||
event: Event,
|
||||
style: Megaphone.Style
|
||||
): Megaphone {
|
||||
return remember {
|
||||
Megaphone.Builder(event, style)
|
||||
.setImage(R.drawable.illustration_toggle_switch)
|
||||
.setTitle("Avengers HQ Destroyed!")
|
||||
.setBody("Where was the 'hero' Spider-Man during the battle?")
|
||||
.setActionButton("*sigh*") { _, _ -> }
|
||||
.setSecondaryButton("Remind me later") { _, _ -> }
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
|
||||
public class MegaphoneViewBuilder {
|
||||
|
||||
@@ -14,42 +15,15 @@ public class MegaphoneViewBuilder {
|
||||
{
|
||||
switch (megaphone.getStyle()) {
|
||||
case BASIC:
|
||||
return buildBasicMegaphone(context, megaphone, listener);
|
||||
case ONBOARDING:
|
||||
case POPUP:
|
||||
ComposeView composeView = new ComposeView(context);
|
||||
MegaphoneComponentKt.setContent(composeView, megaphone, listener);
|
||||
return composeView;
|
||||
case FULLSCREEN:
|
||||
return null;
|
||||
case ONBOARDING:
|
||||
return buildOnboardingMegaphone(context, megaphone, listener);
|
||||
case POPUP:
|
||||
return buildPopupMegaphone(context, megaphone, listener);
|
||||
default:
|
||||
throw new IllegalArgumentException("No view implemented for style!");
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull View buildBasicMegaphone(@NonNull Context context,
|
||||
@NonNull Megaphone megaphone,
|
||||
@NonNull MegaphoneActionController listener)
|
||||
{
|
||||
BasicMegaphoneView view = new BasicMegaphoneView(context);
|
||||
view.present(megaphone, listener);
|
||||
return view;
|
||||
}
|
||||
|
||||
private static @NonNull View buildOnboardingMegaphone(@NonNull Context context,
|
||||
@NonNull Megaphone megaphone,
|
||||
@NonNull MegaphoneActionController listener)
|
||||
{
|
||||
OnboardingMegaphoneView view = new OnboardingMegaphoneView(context);
|
||||
view.present(megaphone, listener);
|
||||
return view;
|
||||
}
|
||||
|
||||
private static @NonNull View buildPopupMegaphone(@NonNull Context context,
|
||||
@NonNull Megaphone megaphone,
|
||||
@NonNull MegaphoneActionController listener)
|
||||
{
|
||||
PopupMegaphoneView view = new PopupMegaphoneView(context);
|
||||
view.present(megaphone, listener);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.megaphone
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.IconButtons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.InviteActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.main.EmptyMegaphoneActionController
|
||||
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity
|
||||
|
||||
/**
|
||||
* The onboarding megaphone (list of cards)
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingMegaphone(
|
||||
megaphoneActionController: MegaphoneActionController,
|
||||
modifier: Modifier = Modifier,
|
||||
onboardingState: OnboardingState = OnboardingState.rememberOnboardingState(megaphoneActionController)
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(bottom = 22.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.Megaphones_get_started),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
|
||||
val onboardingItems = remember(onboardingState.displayState) {
|
||||
OnboardingListItem.entries.filter(onboardingState.displayState::shouldDisplayListItem)
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
) {
|
||||
itemsIndexed(items = onboardingItems) { idx, item ->
|
||||
OnboardingMegaphoneListItem(
|
||||
onboardingListItem = item,
|
||||
onActionClick = {
|
||||
onboardingState.onItemActionClick(item)
|
||||
},
|
||||
onCloseClick = {
|
||||
onboardingState.onItemCloseClick(item)
|
||||
},
|
||||
modifier = if (idx == 0) Modifier.padding(start = 16.dp) else Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single megaphone list item, such as "Invite Friends"
|
||||
*/
|
||||
@Composable
|
||||
private fun OnboardingMegaphoneListItem(
|
||||
onboardingListItem: OnboardingListItem,
|
||||
onActionClick: (OnboardingListItem) -> Unit,
|
||||
onCloseClick: (OnboardingListItem) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(0.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = colorResource(onboardingListItem.cardColor)
|
||||
),
|
||||
modifier = modifier
|
||||
.padding(end = 12.dp)
|
||||
.width(152.dp)
|
||||
.clickable(onClick = { onActionClick(onboardingListItem) })
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
IconButtons.IconButton(
|
||||
onClick = { onCloseClick(onboardingListItem) },
|
||||
size = 48.dp,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_x_24),
|
||||
tint = colorResource(R.color.signal_light_colorOutline),
|
||||
contentDescription = stringResource(R.string.Material3SearchToolbar__close)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 84.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(onboardingListItem.icon),
|
||||
contentDescription = null,
|
||||
tint = colorResource(R.color.signal_light_colorOnSurface),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(onboardingListItem.title),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
color = colorResource(R.color.signal_light_colorOnSurface),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun OnboardingMegaphonePreview() {
|
||||
Previews.Preview {
|
||||
OnboardingMegaphone(
|
||||
megaphoneActionController = EmptyMegaphoneActionController,
|
||||
onboardingState = OnboardingState.rememberOnboardingState()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun OnboardingMegaphoneListItemPreview() {
|
||||
Previews.Preview {
|
||||
OnboardingMegaphoneListItem(
|
||||
onboardingListItem = OnboardingListItem.INVITE,
|
||||
onActionClick = {},
|
||||
onCloseClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a card that can be displayed to the user when showing onboarding content.
|
||||
*/
|
||||
enum class OnboardingListItem(
|
||||
@StringRes val title: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
@ColorRes val cardColor: Int
|
||||
) {
|
||||
GROUP(
|
||||
title = R.string.Megaphones_new_group,
|
||||
icon = R.drawable.symbol_group_24,
|
||||
cardColor = R.color.onboarding_background_1
|
||||
),
|
||||
INVITE(
|
||||
title = R.string.Megaphones_invite_friends,
|
||||
icon = R.drawable.symbol_invite_24,
|
||||
cardColor = R.color.onboarding_background_2
|
||||
),
|
||||
ADD_PHOTO(
|
||||
title = R.string.Megaphones_add_a_profile_photo,
|
||||
icon = R.drawable.symbol_person_circle_24,
|
||||
cardColor = R.color.onboarding_background_4
|
||||
),
|
||||
APPEARANCE(
|
||||
title = R.string.Megaphones_chat_colors,
|
||||
icon = R.drawable.ic_color_24,
|
||||
cardColor = R.color.onboarding_background_3
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the list of displayable cards and drives actions performed by the user.
|
||||
*/
|
||||
abstract class OnboardingState private constructor(
|
||||
initialState: DisplayState = DisplayState(),
|
||||
val megaphoneActionController: MegaphoneActionController
|
||||
) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Grabs an [OnboardingState], keyed to the given [MegaphoneActionController]
|
||||
*/
|
||||
@Composable
|
||||
fun rememberOnboardingState(megaphoneActionController: MegaphoneActionController = EmptyMegaphoneActionController): OnboardingState {
|
||||
return if (LocalInspectionMode.current) {
|
||||
Preview
|
||||
} else {
|
||||
remember(megaphoneActionController) { Real(megaphoneActionController = megaphoneActionController) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The latest display state for the list of onboarding items. An empty list means we can
|
||||
* mark this megaphone as complete.
|
||||
*/
|
||||
var displayState: DisplayState by mutableStateOf(initialState)
|
||||
|
||||
/**
|
||||
* When a list item is clicked.
|
||||
*/
|
||||
abstract fun onItemActionClick(onboardingListItem: OnboardingListItem)
|
||||
|
||||
/**
|
||||
* When a list item close button is clicked.
|
||||
*/
|
||||
abstract fun onItemCloseClick(onboardingListItem: OnboardingListItem)
|
||||
|
||||
/**
|
||||
* Preview implementation, used automatically when rendering previews.
|
||||
*/
|
||||
private object Preview : OnboardingState(
|
||||
initialState = DisplayState(
|
||||
shouldShowNewGroup = true,
|
||||
shouldShowInviteFriends = true,
|
||||
shouldShowAddPhoto = true,
|
||||
shouldShowAppearance = true
|
||||
),
|
||||
megaphoneActionController = EmptyMegaphoneActionController
|
||||
) {
|
||||
override fun onItemCloseClick(onboardingListItem: OnboardingListItem) {
|
||||
displayState = when (onboardingListItem) {
|
||||
OnboardingListItem.GROUP -> displayState.copy(shouldShowNewGroup = false)
|
||||
OnboardingListItem.INVITE -> displayState.copy(shouldShowInviteFriends = false)
|
||||
OnboardingListItem.ADD_PHOTO -> displayState.copy(shouldShowAddPhoto = false)
|
||||
OnboardingListItem.APPEARANCE -> displayState.copy(shouldShowAppearance = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemActionClick(onboardingListItem: OnboardingListItem) = Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Real implementation, used automatically on-device. Backed by SignalStore.
|
||||
*/
|
||||
private class Real(megaphoneActionController: MegaphoneActionController) : OnboardingState(megaphoneActionController = megaphoneActionController) {
|
||||
override fun onItemCloseClick(onboardingListItem: OnboardingListItem) {
|
||||
when (onboardingListItem) {
|
||||
OnboardingListItem.GROUP -> SignalStore.onboarding.setShowNewGroup(false)
|
||||
OnboardingListItem.INVITE -> SignalStore.onboarding.setShowInviteFriends(false)
|
||||
OnboardingListItem.ADD_PHOTO -> SignalStore.onboarding.setShowAddPhoto(false)
|
||||
OnboardingListItem.APPEARANCE -> SignalStore.onboarding.setShowAppearance(false)
|
||||
}
|
||||
|
||||
displayState = DisplayState()
|
||||
|
||||
if (displayState.hasNoVisibleContent()) {
|
||||
megaphoneActionController.onMegaphoneCompleted(Megaphones.Event.ONBOARDING)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemActionClick(onboardingListItem: OnboardingListItem) {
|
||||
when (onboardingListItem) {
|
||||
OnboardingListItem.GROUP -> megaphoneActionController.onMegaphoneNavigationRequested(CreateGroupActivity.newIntent(megaphoneActionController.megaphoneActivity))
|
||||
OnboardingListItem.INVITE -> megaphoneActionController.onMegaphoneNavigationRequested(Intent(megaphoneActionController.megaphoneActivity, InviteActivity::class.java))
|
||||
OnboardingListItem.ADD_PHOTO -> {
|
||||
megaphoneActionController.onMegaphoneNavigationRequested(EditProfileActivity.getIntentForAvatarEdit(megaphoneActionController.megaphoneActivity))
|
||||
SignalStore.onboarding.setShowAddPhoto(false)
|
||||
}
|
||||
OnboardingListItem.APPEARANCE -> {
|
||||
megaphoneActionController.onMegaphoneNavigationRequested(ChatWallpaperActivity.createIntent(megaphoneActionController.megaphoneActivity))
|
||||
SignalStore.onboarding.setShowAppearance(false)
|
||||
}
|
||||
}
|
||||
|
||||
displayState = DisplayState()
|
||||
|
||||
if (displayState.hasNoVisibleContent()) {
|
||||
megaphoneActionController.onMegaphoneCompleted(Megaphones.Event.ONBOARDING)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple display state, driven by [SignalStore] by default.
|
||||
*/
|
||||
data class DisplayState(
|
||||
private val shouldShowNewGroup: Boolean = SignalStore.onboarding.shouldShowNewGroup(),
|
||||
private val shouldShowInviteFriends: Boolean = SignalStore.onboarding.shouldShowInviteFriends(),
|
||||
private val shouldShowAddPhoto: Boolean = SignalStore.onboarding.shouldShowAddPhoto() && !SignalStore.misc.hasEverHadAnAvatar,
|
||||
private val shouldShowAppearance: Boolean = SignalStore.onboarding.shouldShowAppearance()
|
||||
) {
|
||||
fun hasNoVisibleContent(): Boolean = !(shouldShowNewGroup || shouldShowInviteFriends || shouldShowAddPhoto || shouldShowAppearance)
|
||||
|
||||
fun shouldDisplayListItem(onboardingListItem: OnboardingListItem): Boolean {
|
||||
return when (onboardingListItem) {
|
||||
OnboardingListItem.GROUP -> shouldShowNewGroup
|
||||
OnboardingListItem.INVITE -> shouldShowInviteFriends
|
||||
OnboardingListItem.ADD_PHOTO -> shouldShowAddPhoto
|
||||
OnboardingListItem.APPEARANCE -> shouldShowAppearance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.InviteActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.databinding.OnboardingMegaphoneCardBinding;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shows the a fun rail of cards that educate the user about some actions they can take right after
|
||||
* they install the app.
|
||||
*/
|
||||
public class OnboardingMegaphoneView extends FrameLayout {
|
||||
|
||||
private static final String TAG = Log.tag(OnboardingMegaphoneView.class);
|
||||
|
||||
private RecyclerView cardList;
|
||||
|
||||
public OnboardingMegaphoneView(Context context) {
|
||||
super(context);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
public OnboardingMegaphoneView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
private void initialize(@NonNull Context context) {
|
||||
inflate(context, R.layout.onboarding_megaphone, this);
|
||||
|
||||
this.cardList = findViewById(R.id.onboarding_megaphone_list);
|
||||
}
|
||||
|
||||
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener) {
|
||||
this.cardList.setAdapter(new CardAdapter(getContext(), listener));
|
||||
}
|
||||
|
||||
private static class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements ActionClickListener {
|
||||
|
||||
private static final int TYPE_GROUP = 0;
|
||||
private static final int TYPE_INVITE = 1;
|
||||
private static final int TYPE_APPEARANCE = 2;
|
||||
private static final int TYPE_ADD_PHOTO = 3;
|
||||
|
||||
private final Context context;
|
||||
private final MegaphoneActionController controller;
|
||||
private final List<Integer> data;
|
||||
|
||||
CardAdapter(@NonNull Context context, @NonNull MegaphoneActionController controller) {
|
||||
this.context = context;
|
||||
this.controller = controller;
|
||||
this.data = buildData();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
Log.i(TAG, "Nothing to show (constructor)! Considering megaphone completed.");
|
||||
controller.onMegaphoneCompleted(Megaphones.Event.ONBOARDING);
|
||||
}
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CardViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.onboarding_megaphone_card, parent, false);
|
||||
switch (viewType) {
|
||||
case TYPE_GROUP: return new GroupCardViewHolder(view);
|
||||
case TYPE_INVITE: return new InviteCardViewHolder(view);
|
||||
case TYPE_APPEARANCE: return new AppearanceCardViewHolder(view);
|
||||
case TYPE_ADD_PHOTO: return new AddPhotoCardViewHolder(view);
|
||||
default: throw new IllegalStateException("Invalid viewType! " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CardViewHolder holder, int position) {
|
||||
holder.bind(this, controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
data.clear();
|
||||
data.addAll(buildData());
|
||||
if (data.isEmpty()) {
|
||||
Log.i(TAG, "Nothing to show! Considering megaphone completed.");
|
||||
controller.onMegaphoneCompleted(Megaphones.Event.ONBOARDING);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static List<Integer> buildData() {
|
||||
List<Integer> data = new ArrayList<>();
|
||||
|
||||
if (SignalStore.onboarding().shouldShowNewGroup()) {
|
||||
data.add(TYPE_GROUP);
|
||||
}
|
||||
|
||||
if (SignalStore.onboarding().shouldShowInviteFriends()) {
|
||||
data.add(TYPE_INVITE);
|
||||
}
|
||||
|
||||
if (SignalStore.onboarding().shouldShowAddPhoto() && !SignalStore.misc().getHasEverHadAnAvatar()) {
|
||||
data.add(TYPE_ADD_PHOTO);
|
||||
}
|
||||
|
||||
if (SignalStore.onboarding().shouldShowAppearance()) {
|
||||
data.add(TYPE_APPEARANCE);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
private interface ActionClickListener {
|
||||
void onClick();
|
||||
}
|
||||
|
||||
private static abstract class CardViewHolder extends RecyclerView.ViewHolder {
|
||||
private final OnboardingMegaphoneCardBinding binding;
|
||||
|
||||
public CardViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
binding = OnboardingMegaphoneCardBinding.bind(itemView);
|
||||
}
|
||||
|
||||
public void bind(@NonNull ActionClickListener listener, @NonNull MegaphoneActionController controller) {
|
||||
binding.getRoot().setCardBackgroundColor(ContextCompat.getColor(binding.getRoot().getContext(), getBackgroundColor()));
|
||||
binding.icon.setImageResource(getImageRes());
|
||||
binding.text.setText(getButtonStringRes());
|
||||
binding.getRoot().setOnClickListener(v -> {
|
||||
onActionClicked(controller);
|
||||
listener.onClick();
|
||||
});
|
||||
binding.close.setOnClickListener(v -> {
|
||||
onCloseClicked();
|
||||
listener.onClick();
|
||||
});
|
||||
}
|
||||
|
||||
abstract @StringRes int getButtonStringRes();
|
||||
abstract @DrawableRes int getImageRes();
|
||||
abstract @ColorRes int getBackgroundColor();
|
||||
abstract void onActionClicked(@NonNull MegaphoneActionController controller);
|
||||
abstract void onCloseClicked();
|
||||
}
|
||||
|
||||
private static class GroupCardViewHolder extends CardViewHolder {
|
||||
|
||||
public GroupCardViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getButtonStringRes() {
|
||||
return R.string.Megaphones_new_group;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getImageRes() {
|
||||
return R.drawable.symbol_group_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getBackgroundColor() {
|
||||
return R.color.onboarding_background_1;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onActionClicked(@NonNull MegaphoneActionController controller) {
|
||||
controller.onMegaphoneNavigationRequested(CreateGroupActivity.newIntent(controller.getMegaphoneActivity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onCloseClicked() {
|
||||
SignalStore.onboarding().setShowNewGroup(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static class InviteCardViewHolder extends CardViewHolder {
|
||||
|
||||
public InviteCardViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getButtonStringRes() {
|
||||
return R.string.Megaphones_invite_friends;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getImageRes() {
|
||||
return R.drawable.symbol_invite_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getBackgroundColor() {
|
||||
return R.color.onboarding_background_2;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onActionClicked(@NonNull MegaphoneActionController controller) {
|
||||
controller.onMegaphoneNavigationRequested(new Intent(controller.getMegaphoneActivity(), InviteActivity.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onCloseClicked() {
|
||||
SignalStore.onboarding().setShowInviteFriends(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AppearanceCardViewHolder extends CardViewHolder {
|
||||
|
||||
public AppearanceCardViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getButtonStringRes() {
|
||||
return R.string.Megaphones_chat_colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getImageRes() {
|
||||
return R.drawable.ic_color_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getBackgroundColor() {
|
||||
return R.color.onboarding_background_3;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onActionClicked(@NonNull MegaphoneActionController controller) {
|
||||
controller.onMegaphoneNavigationRequested(ChatWallpaperActivity.createIntent(controller.getMegaphoneActivity()));
|
||||
SignalStore.onboarding().setShowAppearance(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onCloseClicked() {
|
||||
SignalStore.onboarding().setShowAppearance(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddPhotoCardViewHolder extends CardViewHolder {
|
||||
|
||||
public AddPhotoCardViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getButtonStringRes() {
|
||||
return R.string.Megaphones_add_a_profile_photo;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getImageRes() {
|
||||
return R.drawable.symbol_person_circle_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getBackgroundColor() {
|
||||
return R.color.onboarding_background_4;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onActionClicked(@NonNull MegaphoneActionController controller) {
|
||||
controller.onMegaphoneNavigationRequested(EditProfileActivity.getIntentForAvatarEdit(controller.getMegaphoneActivity()));
|
||||
SignalStore.onboarding().setShowAddPhoto(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onCloseClicked() {
|
||||
SignalStore.onboarding().setShowAddPhoto(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class PopupMegaphoneView extends FrameLayout {
|
||||
|
||||
private LottieAnimationView image;
|
||||
private TextView titleText;
|
||||
private TextView bodyText;
|
||||
private View xButton;
|
||||
|
||||
private Megaphone megaphone;
|
||||
private MegaphoneActionController megaphoneListener;
|
||||
|
||||
public PopupMegaphoneView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public PopupMegaphoneView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(@NonNull Context context) {
|
||||
inflate(context, R.layout.popup_megaphone_view, this);
|
||||
|
||||
this.image = findViewById(R.id.popup_megaphone_image);
|
||||
this.titleText = findViewById(R.id.popup_megaphone_title);
|
||||
this.bodyText = findViewById(R.id.popup_megaphone_body);
|
||||
this.xButton = findViewById(R.id.popup_x);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController megaphoneListener) {
|
||||
this.megaphone = megaphone;
|
||||
this.megaphoneListener = megaphoneListener;
|
||||
|
||||
if (megaphone.getImageRequestBuilder() != null) {
|
||||
image.setVisibility(VISIBLE);
|
||||
megaphone.getImageRequestBuilder().into(image);
|
||||
} else if (megaphone.getLottieRes() != 0) {
|
||||
image.setVisibility(VISIBLE);
|
||||
image.setAnimation(megaphone.getLottieRes());
|
||||
image.playAnimation();
|
||||
} else {
|
||||
image.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getTitle().hasText()) {
|
||||
titleText.setVisibility(VISIBLE);
|
||||
titleText.setText(megaphone.getTitle().resolve(getContext()));
|
||||
} else {
|
||||
titleText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getBody().hasText()) {
|
||||
bodyText.setVisibility(VISIBLE);
|
||||
bodyText.setText(megaphone.getBody().resolve(getContext()));
|
||||
} else {
|
||||
bodyText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.hasButton()) {
|
||||
xButton.setOnClickListener(v -> megaphone.getButtonClickListener().onEvent(megaphone, megaphoneListener));
|
||||
} else {
|
||||
xButton.setOnClickListener(v -> megaphoneListener.onMegaphoneCompleted(megaphone.getEvent()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user