mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-23 03:25:06 +03:00
Compare commits
3 Commits
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.2.1</string>
|
<string>0.2.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>021</string>
|
<string>022</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ private fun PreviewBar(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 12.dp, end = 12.dp, top = 8.dp),
|
.padding(start = 12.dp, end = 6.dp, top = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -188,13 +188,12 @@ fun ChatScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Column(
|
Column(Modifier.fillMaxWidth()) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = panelState.title,
|
text = panelState.title,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = currentTypingUsers.isNotEmpty(),
|
targetState = currentTypingUsers.isNotEmpty(),
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
@@ -207,9 +206,6 @@ fun ChatScreen(
|
|||||||
typingUsers = currentTypingUsers.map { it.username },
|
typingUsers = currentTypingUsers.map { it.username },
|
||||||
modifier = Modifier.padding(top = 2.dp)
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// Empty space to maintain height
|
|
||||||
Box(modifier = Modifier.height(0.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class PublicChatPanel(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
typingHandler.typingUsers.collect { users ->
|
typingHandler.typingUsers.collect { users ->
|
||||||
Logger.d("PublicChatPanel", "Typing users updated in handler: ${users.map { it.username }}")
|
Logger.d("PublicChatPanel", "Typing users updated in handler: ${users.map { it.username }}")
|
||||||
updateState { it.copy(typingUsers = users) }
|
updateState { it.copy(typingUsers = users.filter { it.userId != currentUserId }) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ class PublicChatPanel(
|
|||||||
}
|
}
|
||||||
setHasMoreMessages(false) // TODO: Implement has_more from API
|
setHasMoreMessages(false) // TODO: Implement has_more from API
|
||||||
messagesLoaded = true
|
messagesLoaded = true
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
// Handle error
|
// Handle error
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -84,7 +84,7 @@ class PublicChatPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setHasMoreMessages(false) // TODO: Implement has_more from API
|
setHasMoreMessages(false) // TODO: Implement has_more from API
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
// Handle error
|
// Handle error
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingMore(false)
|
setLoadingMore(false)
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import androidx.compose.animation.core.animateFloat
|
|||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.keyframes
|
import androidx.compose.animation.core.keyframes
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -16,7 +19,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
@@ -63,67 +65,54 @@ private fun formatTypingText(typingUsers: List<String>): String {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TypingDots() {
|
private fun TypingDots() {
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "typing_dots")
|
val infiniteTransition = rememberInfiniteTransition(label = "typing_dots")
|
||||||
|
val duration = 1000
|
||||||
|
val initialScale = 1f
|
||||||
|
val targetScale = 1.3f
|
||||||
|
|
||||||
val dot1Alpha by infiniteTransition.animateFloat(
|
// Функция-хелпер для создания анимации с задержкой
|
||||||
initialValue = 0.3f,
|
@Composable
|
||||||
targetValue = 1f,
|
fun animateDotScale(delay: Int) = infiniteTransition.animateFloat(
|
||||||
|
initialValue = initialScale,
|
||||||
|
targetValue = initialScale, // Возвращаемся в начало
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec = infiniteRepeatable(
|
||||||
animation = keyframes {
|
animation = keyframes {
|
||||||
durationMillis = 1400
|
durationMillis = duration
|
||||||
0.3f at 0
|
initialScale at delay // Начало подъема
|
||||||
1f at 200
|
targetScale at delay + 200 // Пик
|
||||||
0.3f at 400
|
initialScale at delay + 400 // Возврат
|
||||||
|
initialScale at duration // Удержание до конца цикла
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
label = "dot1"
|
label = "dot_scale"
|
||||||
)
|
)
|
||||||
|
|
||||||
val dot2Alpha by infiniteTransition.animateFloat(
|
val dot1Scale by animateDotScale(delay = 0)
|
||||||
initialValue = 0.3f,
|
val dot2Scale by animateDotScale(delay = 200)
|
||||||
targetValue = 1f,
|
val dot3Scale by animateDotScale(delay = 400)
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = keyframes {
|
|
||||||
durationMillis = 1400
|
|
||||||
0.3f at 200
|
|
||||||
1f at 400
|
|
||||||
0.3f at 600
|
|
||||||
}
|
|
||||||
),
|
|
||||||
label = "dot2"
|
|
||||||
)
|
|
||||||
|
|
||||||
val dot3Alpha by infiniteTransition.animateFloat(
|
Row(
|
||||||
initialValue = 0.3f,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
targetValue = 1f,
|
horizontalArrangement = Arrangement.spacedBy(2.dp) // Вместо Spacer
|
||||||
animationSpec = infiniteRepeatable(
|
) {
|
||||||
animation = keyframes {
|
Dot(scale = dot1Scale, maxScale = targetScale)
|
||||||
durationMillis = 1400
|
Dot(scale = dot2Scale, maxScale = targetScale)
|
||||||
0.3f at 400
|
Dot(scale = dot3Scale, maxScale = targetScale)
|
||||||
1f at 600
|
|
||||||
0.3f at 800
|
|
||||||
}
|
|
||||||
),
|
|
||||||
label = "dot3"
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Dot(alpha = dot1Alpha)
|
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
|
||||||
Dot(alpha = dot2Alpha)
|
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
|
||||||
Dot(alpha = dot3Alpha)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Dot(alpha: Float) {
|
@Suppress("SameParameterValue")
|
||||||
|
private fun Dot(maxScale: Float, scale: Float) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(4.dp * maxScale),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(4.dp)
|
.width(4.dp * scale)
|
||||||
.height(4.dp)
|
.height(4.dp * scale),
|
||||||
.alpha(alpha),
|
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
+40
-8
@@ -52,7 +52,6 @@ step() { echo -e "\n${CYAN}${BOLD}→${NC} ${BOLD}$1${NC}"; }
|
|||||||
substep() { echo -e " ${GREEN}•${NC} $1"; }
|
substep() { echo -e " ${GREEN}•${NC} $1"; }
|
||||||
|
|
||||||
echo -e "${MAGENTA}${BOLD}🚀 FromChat KMP Release Pipeline${NC}"
|
echo -e "${MAGENTA}${BOLD}🚀 FromChat KMP Release Pipeline${NC}"
|
||||||
[[ "$IS_PRERELEASE" == true ]] && info "Prerelease mode enabled via --pre flag"
|
|
||||||
|
|
||||||
# --- Настройка памяти для предотвращения OutOfMemory ---
|
# --- Настройка памяти для предотвращения OutOfMemory ---
|
||||||
export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx8g -Dkotlin.daemon.jvm.options=-Xmx8g"
|
export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx8g -Dkotlin.daemon.jvm.options=-Xmx8g"
|
||||||
@@ -67,13 +66,14 @@ PREVTAG=${TAGS[1]:-$TAG}
|
|||||||
substep "Current tag: ${YELLOW}$TAG${NC}"
|
substep "Current tag: ${YELLOW}$TAG${NC}"
|
||||||
substep "Previous tag: ${YELLOW}$PREVTAG${NC}"
|
substep "Previous tag: ${YELLOW}$PREVTAG${NC}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2001
|
||||||
BUILD_NUMBER=$(echo "$TAG" | sed 's/[^0-9]//g')
|
BUILD_NUMBER=$(echo "$TAG" | sed 's/[^0-9]//g')
|
||||||
[[ -z "$BUILD_NUMBER" ]] && BUILD_NUMBER=1
|
[[ -z "$BUILD_NUMBER" ]] && BUILD_NUMBER=1
|
||||||
|
|
||||||
# --- Сборка Android ---
|
# --- Сборка Android ---
|
||||||
build_android() {
|
build_android() {
|
||||||
step "Building Android Release"
|
step "Building Android Release"
|
||||||
if ./gradlew :app:android:assembleRelease -PbuildNumber="$BUILD_NUMBER" -Dorg.gradle.jvmargs="-Xmx8g"; then
|
if ./gradlew :app:android:assembleRelease; then
|
||||||
APK_SRC=$(find . -name "*release.apk" | head -n 1)
|
APK_SRC=$(find . -name "*release.apk" | head -n 1)
|
||||||
|
|
||||||
if [[ -f "$APK_SRC" ]]; then
|
if [[ -f "$APK_SRC" ]]; then
|
||||||
@@ -101,6 +101,7 @@ build_ios() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||||
|
# shellcheck disable=SC2155
|
||||||
[[ ! -d "$DEVELOPER_DIR" ]] && export DEVELOPER_DIR=$(xcode-select -p)
|
[[ ! -d "$DEVELOPER_DIR" ]] && export DEVELOPER_DIR=$(xcode-select -p)
|
||||||
|
|
||||||
IOS_PROJECT_DIR="app/ios"
|
IOS_PROJECT_DIR="app/ios"
|
||||||
@@ -150,6 +151,28 @@ build_ios() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Подготовка описания ---
|
||||||
|
prepare_description() {
|
||||||
|
DESC_FILE=".release_desc.md"
|
||||||
|
echo -e "<!--\nEnter the release description. Leave empty for no description.\n-->" > "$DESC_FILE"
|
||||||
|
|
||||||
|
# Открываем nano. Пользователь должен отредактировать файл.
|
||||||
|
nano "$DESC_FILE"
|
||||||
|
|
||||||
|
# Удаляем комментарии <!-- ... --> и лишние пробелы/пустые строки
|
||||||
|
# Используем perl для многострочного удаления комментариев
|
||||||
|
CLEAN_DESC=$(perl -0777 -pe 's/<!--.*?-->//gs' "$DESC_FILE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||||
|
|
||||||
|
rm -f "$DESC_FILE"
|
||||||
|
|
||||||
|
if [[ -n "$CLEAN_DESC" ]]; then
|
||||||
|
echo "$CLEAN_DESC" > .final_notes.md
|
||||||
|
NOTES_ARG=("-F" "./.final_notes.md")
|
||||||
|
else
|
||||||
|
NOTES_ARG=("--generate-notes")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# --- Публикация в GitHub ---
|
# --- Публикация в GitHub ---
|
||||||
publish_github() {
|
publish_github() {
|
||||||
step "GitHub Deployment"
|
step "GitHub Deployment"
|
||||||
@@ -161,36 +184,45 @@ publish_github() {
|
|||||||
substep "Pushing tags..."
|
substep "Pushing tags..."
|
||||||
git push --tags > /dev/null 2>&1 || true
|
git push --tags > /dev/null 2>&1 || true
|
||||||
|
|
||||||
# Определяем флаг пререлиза на основе тега ИЛИ аргумента --pre
|
|
||||||
prerelease_flag=""
|
prerelease_flag=""
|
||||||
if [[ "$IS_PRERELEASE" == true ]] || [[ "$TAG" == v*-pre* ]]; then
|
if [[ "$IS_PRERELEASE" == true ]] || [[ "$TAG" == v*-pre* ]]; then
|
||||||
prerelease_flag="--prerelease"
|
prerelease_flag="--prerelease"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Собираем существующие файлы в массив
|
|
||||||
ASSETS=()
|
ASSETS=()
|
||||||
[[ -f "$ANDROID_ASSET" ]] && ASSETS+=("$ANDROID_ASSET")
|
[[ -f "$ANDROID_ASSET" ]] && ASSETS+=("$ANDROID_ASSET")
|
||||||
[[ -f "$IOS_ASSET" ]] && ASSETS+=("$IOS_ASSET")
|
[[ -f "$IOS_ASSET" ]] && ASSETS+=("$IOS_ASSET")
|
||||||
|
|
||||||
if [ ${#ASSETS[@]} -eq 0 ]; then
|
if [ ${#ASSETS[@]} -eq 0 ]; then
|
||||||
error "No assets found to upload. Check build logs."
|
error "No assets found to upload."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем наличие релиза и создаем/обновляем
|
# Подготавливаем описание перед созданием
|
||||||
|
prepare_description
|
||||||
|
|
||||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||||
substep "Updating existing release ${YELLOW}$TAG${NC}..."
|
substep "Updating existing release ${YELLOW}$TAG${NC}..."
|
||||||
[[ -n "$prerelease_flag" ]] && gh release edit "$TAG" "$prerelease_flag"
|
[[ -n "$prerelease_flag" ]] && gh release edit "$TAG" "$prerelease_flag"
|
||||||
|
|
||||||
|
# Если есть кастомные ноты, обновляем их
|
||||||
|
if [[ -f .final_notes.md ]]; then
|
||||||
|
gh release edit "$TAG" -F ./.final_notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
gh release upload "$TAG" "${ASSETS[@]}" --clobber
|
gh release upload "$TAG" "${ASSETS[@]}" --clobber
|
||||||
else
|
else
|
||||||
substep "Creating new release ${YELLOW}$TAG${NC}..."
|
substep "Creating new release ${YELLOW}$TAG${NC}..."
|
||||||
|
# gh release create принимает либо --generate-notes, либо --notes-file
|
||||||
gh release create "$TAG" \
|
gh release create "$TAG" \
|
||||||
--generate-notes \
|
"${NOTES_ARG[@]}" \
|
||||||
--notes-start-tag "${PREVTAG:-$TAG}" \
|
--notes-start-tag "${PREVTAG:-$TAG}" \
|
||||||
$prerelease_flag \
|
$prerelease_flag \
|
||||||
"${ASSETS[@]}"
|
"${ASSETS[@]}"
|
||||||
fi
|
fi
|
||||||
success "Assets uploaded to GitHub Release"
|
|
||||||
|
rm -f .final_notes.md
|
||||||
|
success "Assets and notes uploaded to GitHub Release"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Основной процесс ---
|
# --- Основной процесс ---
|
||||||
|
|||||||
Reference in New Issue
Block a user