mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Reimplement megaphone UI in compose.
This commit is contained in:
@@ -1038,10 +1038,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
} else {
|
||||
megaphoneContainer.get().setVisibility(View.GONE);
|
||||
|
||||
if (megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, this);
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onMegaphoneVisible(megaphone);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:strokeColor="@color/signal_colorOutline_38"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardBackgroundColor="@color/signal_colorBackground"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardPreventCornerOverlap="false"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:contentPadding="0dp">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/basic_megaphone_image"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/basic_megaphone_title"
|
||||
style="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/basic_megaphone_image"
|
||||
app:layout_constraintTop_toTopOf="@id/basic_megaphone_image"
|
||||
tools:text="Avengers HQ Destroyed!" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/basic_megaphone_body"
|
||||
style="@style/Signal.Text.BodyMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textColor="@color/megaphone_body_text_color"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/basic_megaphone_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_title"
|
||||
tools:text="Where was the 'hero' Spider-Man during the battle?" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/basic_megaphone_content_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="basic_megaphone_image,basic_megaphone_body,basic_megaphone_title" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/basic_megaphone_action"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/signal_colorPrimary"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/basic_megaphone_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
||||
app:layout_constraintWidth_max="wrap"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
tools:text="*sigh*"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/basic_megaphone_secondary"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/Megaphones_remind_me_later"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/signal_colorPrimary"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toStartOf="@id/basic_megaphone_action"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
||||
app:layout_constraintWidth_max="wrap"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</merge>
|
||||
@@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="10dp"
|
||||
tools:parentTag="android.widget.FrameLayout"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/megaphone_onboarding_gradient" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/signal_background_primary"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/onboarding_megaphone_title"
|
||||
style="@style/TextAppearance.Signal.Title2.Bold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/Megaphones_get_started"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/onboarding_megaphone_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintTop_toBottomOf="@id/onboarding_megaphone_title"
|
||||
tools:listitem="@layout/onboarding_megaphone_card" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView 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="152dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:cardCornerRadius="28dp"
|
||||
app:cardElevation="0dp"
|
||||
tools:cardBackgroundColor="@color/onboarding_background_1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="84dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/close"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/Material3SearchToolbar__close"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/symbol_x_24"
|
||||
app:tint="@color/signal_light_colorOutline" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintBottom_toTopOf="@id/text"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:tint="@color/signal_light_colorOnSurface"
|
||||
tools:srcCompat="@drawable/symbol_group_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/Signal.Text.LabelMedium"
|
||||
android:textColor="@color/signal_light_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/icon"
|
||||
tools:text="Set some profile picture so folks know who you are." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:cardBackgroundColor="@color/megaphone_background_color"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="6dp"
|
||||
app:cardPreventCornerOverlap="false"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:contentPadding="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/signal_background_secondary"
|
||||
android:clickable="true"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/popup_megaphone_image"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/popup_megaphone_title"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
app:layout_constraintEnd_toStartOf="@id/popup_x"
|
||||
app:layout_constraintStart_toEndOf="@id/popup_megaphone_image"
|
||||
app:layout_constraintTop_toTopOf="@id/popup_megaphone_image"
|
||||
tools:text="Avengers HQ Destroyed!" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/popup_megaphone_body"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/megaphone_body_text_color"
|
||||
app:layout_constraintEnd_toStartOf="@id/popup_x"
|
||||
app:layout_constraintStart_toStartOf="@id/popup_megaphone_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/popup_megaphone_title"
|
||||
tools:text="Where was the 'hero' Spider-Man during the battle?" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/popup_x"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:paddingStart="12.5dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingEnd="15.5dp"
|
||||
android:paddingBottom="14dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_x_20" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</merge>
|
||||
Reference in New Issue
Block a user