Skip to content

Commit c1730c3

Browse files
committed
feat(telecom): init
Signed-off-by: Infi <[email protected]>
1 parent 14353c7 commit c1730c3

File tree

7 files changed

+440
-0
lines changed

7 files changed

+440
-0
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ dependencies {
256256
implementation("androidx.webkit:webkit:1.14.0")
257257
implementation("androidx.core:core-splashscreen:1.2.0-beta02")
258258
implementation("androidx.palette:palette-ktx:1.0.0")
259+
implementation("androidx.core:core-telecom:1.0.0")
259260

260261
// Libraries used for legacy View-based UI
261262
implementation("androidx.constraintlayout:constraintlayout:2.2.1")

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<uses-permission android:name="android.permission.INTERNET" />
66
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
77
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8+
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
89

910
<!-- Up to Android 10, we need the following to take photos from the camera. -->
1011
<uses-permission
@@ -138,6 +139,11 @@
138139
android:configChanges="orientation|screenSize"
139140
android:theme="@style/Theme.Revolt" />
140141

142+
<activity
143+
android:name=".activities.voice.IncomingActivity"
144+
android:configChanges="orientation|screenSize"
145+
android:theme="@style/Theme.Revolt" />
146+
141147
<!-- Backport photo picker via Google Play Services -->
142148
<service
143149
android:name="com.google.android.gms.metadata.ModuleDependencies"
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package chat.revolt.activities.voice
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.compose.animation.animateColor
8+
import androidx.compose.animation.core.FastOutSlowInEasing
9+
import androidx.compose.animation.core.RepeatMode
10+
import androidx.compose.animation.core.animateFloatAsState
11+
import androidx.compose.animation.core.infiniteRepeatable
12+
import androidx.compose.animation.core.rememberInfiniteTransition
13+
import androidx.compose.animation.core.tween
14+
import androidx.compose.foundation.background
15+
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
16+
import androidx.compose.foundation.gestures.AnchoredDraggableState
17+
import androidx.compose.foundation.gestures.DraggableAnchors
18+
import androidx.compose.foundation.gestures.Orientation
19+
import androidx.compose.foundation.gestures.anchoredDraggable
20+
import androidx.compose.foundation.gestures.animateTo
21+
import androidx.compose.foundation.isSystemInDarkTheme
22+
import androidx.compose.foundation.layout.Arrangement
23+
import androidx.compose.foundation.layout.Box
24+
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.Spacer
26+
import androidx.compose.foundation.layout.aspectRatio
27+
import androidx.compose.foundation.layout.fillMaxHeight
28+
import androidx.compose.foundation.layout.fillMaxSize
29+
import androidx.compose.foundation.layout.fillMaxWidth
30+
import androidx.compose.foundation.layout.height
31+
import androidx.compose.foundation.layout.offset
32+
import androidx.compose.foundation.layout.padding
33+
import androidx.compose.foundation.layout.width
34+
import androidx.compose.foundation.shape.CircleShape
35+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
36+
import androidx.compose.material3.Icon
37+
import androidx.compose.material3.LocalContentColor
38+
import androidx.compose.material3.MaterialTheme
39+
import androidx.compose.material3.Text
40+
import androidx.compose.runtime.Composable
41+
import androidx.compose.runtime.CompositionLocalProvider
42+
import androidx.compose.runtime.LaunchedEffect
43+
import androidx.compose.runtime.SideEffect
44+
import androidx.compose.runtime.getValue
45+
import androidx.compose.runtime.mutableFloatStateOf
46+
import androidx.compose.runtime.remember
47+
import androidx.compose.runtime.setValue
48+
import androidx.compose.ui.Alignment
49+
import androidx.compose.ui.Modifier
50+
import androidx.compose.ui.draw.clip
51+
import androidx.compose.ui.draw.rotate
52+
import androidx.compose.ui.graphics.Brush
53+
import androidx.compose.ui.graphics.lerp
54+
import androidx.compose.ui.platform.LocalDensity
55+
import androidx.compose.ui.res.painterResource
56+
import androidx.compose.ui.text.font.FontWeight
57+
import androidx.compose.ui.unit.IntOffset
58+
import androidx.compose.ui.unit.dp
59+
import androidx.compose.ui.unit.sp
60+
import chat.revolt.R
61+
import chat.revolt.composables.generic.Presence
62+
import chat.revolt.composables.generic.RemoteImage
63+
import chat.revolt.composables.generic.presenceColour
64+
import chat.revolt.ui.theme.RevoltTheme
65+
import chat.revolt.ui.theme.Theme
66+
import kotlinx.coroutines.delay
67+
import kotlin.math.roundToInt
68+
69+
class IncomingActivity : ComponentActivity() {
70+
override fun onCreate(savedInstanceState: Bundle?) {
71+
super.onCreate(savedInstanceState)
72+
73+
enableEdgeToEdge()
74+
75+
setContent {
76+
IncomingCall()
77+
}
78+
}
79+
}
80+
81+
@Composable
82+
fun IncomingCall() {
83+
RevoltTheme(
84+
requestedTheme = if (isSystemInDarkTheme()) Theme.Revolt else Theme.Light
85+
) {
86+
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
87+
IncomingCallInner()
88+
}
89+
}
90+
}
91+
92+
private enum class CallSwiperState {
93+
Initial,
94+
Accept,
95+
Decline
96+
}
97+
98+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
99+
@Composable
100+
fun IncomingCallInner() {
101+
val bgInfinite = rememberInfiniteTransition(label = "Background")
102+
val bgColour by bgInfinite.animateColor(
103+
initialValue = MaterialTheme.colorScheme.surfaceContainerHighest,
104+
targetValue = MaterialTheme.colorScheme.surfaceContainerLowest,
105+
label = "Background Colour",
106+
animationSpec = infiniteRepeatable(
107+
animation = tween(1000, 0, FastOutSlowInEasing),
108+
repeatMode = RepeatMode.Reverse
109+
)
110+
)
111+
112+
var swiperLabelColourLerp by remember { mutableFloatStateOf(0f) }
113+
val swiperLabelColourLerpAnim by animateFloatAsState(
114+
targetValue = swiperLabelColourLerp,
115+
animationSpec = tween(250),
116+
label = "Swiper Label Colour Lerp state"
117+
)
118+
LaunchedEffect(Unit) {
119+
while (true) {
120+
swiperLabelColourLerp = 0f
121+
delay(1000)
122+
swiperLabelColourLerp = 1f
123+
delay(4000)
124+
}
125+
}
126+
127+
val swiperState = remember { AnchoredDraggableState(CallSwiperState.Initial) }
128+
val swiperWidth = 330.dp
129+
val swiperWidthThird = swiperWidth / 3
130+
val density = LocalDensity.current
131+
val swiperWidthPx = with(density) { swiperWidth.toPx() }
132+
val swiperThirdPx = with(density) { swiperWidthThird.toPx() }
133+
SideEffect {
134+
swiperState.updateAnchors(
135+
DraggableAnchors {
136+
CallSwiperState.Decline at 35f
137+
CallSwiperState.Initial at (swiperWidthPx / 2f) - (swiperThirdPx / 2f)
138+
CallSwiperState.Accept at swiperWidthPx - swiperThirdPx - 35f
139+
}
140+
)
141+
}
142+
143+
LaunchedEffect(swiperState.currentValue) {
144+
when (swiperState.currentValue) {
145+
CallSwiperState.Accept -> {
146+
swiperState.animateTo(CallSwiperState.Initial)
147+
}
148+
149+
CallSwiperState.Decline -> {
150+
swiperState.animateTo(CallSwiperState.Initial)
151+
}
152+
153+
else -> {}
154+
}
155+
}
156+
157+
val swiperRotation = remember(swiperState.offset) {
158+
if (swiperState.offset.isNaN()) 0f
159+
else {
160+
val declineAnchor = 35f
161+
val initialAnchor = (swiperWidthPx / 2f) - (swiperThirdPx / 2f)
162+
val acceptAnchor = swiperWidthPx - swiperThirdPx - 35f
163+
164+
when {
165+
swiperState.offset <= initialAnchor -> {
166+
// Moving towards decline: interpolate from 0° to -90°
167+
val progress =
168+
(initialAnchor - swiperState.offset) / (initialAnchor - declineAnchor)
169+
-225f * progress.coerceIn(0f, 1f)
170+
}
171+
172+
swiperState.offset >= initialAnchor -> {
173+
// Moving towards accept: interpolate from 0° to 90°
174+
val progress =
175+
(swiperState.offset - initialAnchor) / (acceptAnchor - initialAnchor)
176+
45f * progress.coerceIn(0f, 1f)
177+
}
178+
179+
else -> 0f // At initial position
180+
}
181+
}
182+
}
183+
184+
Box(
185+
Modifier
186+
.fillMaxSize()
187+
.background(bgColour),
188+
contentAlignment = Alignment.Center
189+
) {
190+
Column(
191+
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
192+
horizontalAlignment = Alignment.CenterHorizontally,
193+
) {
194+
Text(
195+
"Incoming Call on Revolt",
196+
style = MaterialTheme.typography.headlineMedium,
197+
fontSize = 24.sp
198+
)
199+
Text(
200+
"cat",
201+
style = MaterialTheme.typography.displayLargeEmphasized,
202+
fontWeight = FontWeight.SemiBold
203+
)
204+
with(density) {
205+
RemoteImage(
206+
url = "https://cdn.revoltusercontent.com/attachments/K1CDpnvORz2fzUhgq47mcL7N4gccWGqNYYeGaJVvyp/image.png",
207+
description = null,
208+
modifier = Modifier
209+
.clip(CircleShape)
210+
.fillMaxWidth(0.5f)
211+
.aspectRatio(1f),
212+
)
213+
}
214+
Spacer(Modifier.fillMaxHeight(.33f))
215+
Box(
216+
modifier = Modifier
217+
.clip(CircleShape)
218+
.height(84.dp)
219+
.width(swiperWidth)
220+
.then(
221+
if (isSystemInDarkTheme()) Modifier
222+
.background(MaterialTheme.colorScheme.surfaceVariant)
223+
.background(
224+
Brush.linearGradient(
225+
listOf(
226+
presenceColour(Presence.Dnd).copy(alpha = 0.05f),
227+
presenceColour(Presence.Online).copy(alpha = 0.05f),
228+
)
229+
)
230+
) else Modifier.background(MaterialTheme.colorScheme.surfaceBright)
231+
),
232+
) {
233+
Text(
234+
"Decline",
235+
color = lerp(
236+
presenceColour(Presence.Dnd),
237+
MaterialTheme.colorScheme.onSurface,
238+
swiperLabelColourLerpAnim
239+
),
240+
modifier = Modifier
241+
.align(Alignment.CenterStart)
242+
.padding(start = 30.dp)
243+
)
244+
Text(
245+
"Accept",
246+
color = lerp(
247+
presenceColour(Presence.Online),
248+
MaterialTheme.colorScheme.onSurface,
249+
swiperLabelColourLerpAnim
250+
),
251+
modifier = Modifier
252+
.align(Alignment.CenterEnd)
253+
.padding(end = 30.dp)
254+
)
255+
Box(
256+
contentAlignment = Alignment.Center,
257+
modifier = Modifier
258+
.width(swiperWidthThird)
259+
.height(64.dp)
260+
.offset {
261+
IntOffset(
262+
x = swiperState.requireOffset().roundToInt(),
263+
y = with(density) { (84.dp - 64.dp).toPx() / 2 }.roundToInt()
264+
)
265+
}
266+
.anchoredDraggable(
267+
swiperState,
268+
Orientation.Horizontal,
269+
flingBehavior =
270+
AnchoredDraggableDefaults.flingBehavior(
271+
swiperState,
272+
positionalThreshold = { distance -> distance * 0.25f },
273+
),
274+
)
275+
.clip(CircleShape)
276+
.background(MaterialTheme.colorScheme.tertiaryContainer)
277+
) {
278+
Icon(
279+
painter = painterResource(R.drawable.icn_call_24dp__fill),
280+
contentDescription = null,
281+
tint = MaterialTheme.colorScheme.onTertiaryContainer,
282+
modifier = Modifier
283+
.rotate(swiperRotation)
284+
)
285+
}
286+
}
287+
}
288+
}
289+
}

app/src/main/java/chat/revolt/screens/labs/LabsHomeScreen.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ fun LabsHomeScreen(navController: NavController, topNav: NavController) {
219219
}
220220
)
221221
HorizontalDivider()
222+
ListItem(
223+
headlineContent = {
224+
Text("Telecom")
225+
},
226+
modifier = Modifier.clickable {
227+
navController.navigate("sandboxes/telecom")
228+
}
229+
)
230+
HorizontalDivider()
222231
}
223232
}
224233
}

app/src/main/java/chat/revolt/screens/labs/LabsRootScreen.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import chat.revolt.screens.labs.ui.sandbox.GradientEditorSandbox
1919
import chat.revolt.screens.labs.ui.sandbox.JBMSandbox
2020
import chat.revolt.screens.labs.ui.sandbox.NewCardSandboxScreen
2121
import chat.revolt.screens.labs.ui.sandbox.SettingsDslSandbox
22+
import chat.revolt.screens.labs.ui.sandbox.TelecomSandbox
2223

2324
annotation class LabsFeature
2425

@@ -87,6 +88,9 @@ fun LabsRootScreen(topNav: NavController) {
8788
composable("sandboxes/newcard") {
8889
NewCardSandboxScreen(labsNav)
8990
}
91+
composable("sandboxes/telecom") {
92+
TelecomSandbox(labsNav)
93+
}
9094
}
9195
}
9296
}

0 commit comments

Comments
 (0)