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 {
|
} else {
|
||||||
megaphoneContainer.get().setVisibility(View.GONE);
|
megaphoneContainer.get().setVisibility(View.GONE);
|
||||||
|
|
||||||
if (megaphone.getOnVisibleListener() != null) {
|
|
||||||
megaphone.getOnVisibleListener().onEvent(megaphone, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.onMegaphoneVisible(megaphone);
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
|
|
||||||
public class MegaphoneViewBuilder {
|
public class MegaphoneViewBuilder {
|
||||||
|
|
||||||
@@ -14,42 +15,15 @@ public class MegaphoneViewBuilder {
|
|||||||
{
|
{
|
||||||
switch (megaphone.getStyle()) {
|
switch (megaphone.getStyle()) {
|
||||||
case BASIC:
|
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:
|
case FULLSCREEN:
|
||||||
return null;
|
return null;
|
||||||
case ONBOARDING:
|
|
||||||
return buildOnboardingMegaphone(context, megaphone, listener);
|
|
||||||
case POPUP:
|
|
||||||
return buildPopupMegaphone(context, megaphone, listener);
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("No view implemented for style!");
|
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