feat:发帖/编辑帖子可以选择帖子的可见性
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
||||
applicationId = "com.memory.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 9
|
||||
versionName = "1.2.5"
|
||||
versionCode = 10
|
||||
versionName = "1.2.6"
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
||||
buildConfigField("int", "VERSION_CODE", "9")
|
||||
buildConfigField("int", "VERSION_CODE", "10")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -20,6 +20,7 @@ data class Post(
|
||||
val id: Long,
|
||||
@SerialName("user_id") val userId: Long,
|
||||
val content: String,
|
||||
val visibility: Int = 0, // 0=所有人可见, 1=仅自己可见
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String? = null,
|
||||
val user: User? = null,
|
||||
@@ -85,7 +86,8 @@ data class LoginResponse(
|
||||
@Serializable
|
||||
data class CreatePostRequest(
|
||||
val content: String,
|
||||
@SerialName("media_ids") val mediaIds: List<String> = emptyList()
|
||||
@SerialName("media_ids") val mediaIds: List<String> = emptyList(),
|
||||
val visibility: Int = 0 // 0=所有人可见, 1=仅自己可见
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -101,7 +103,8 @@ data class AddReactionRequest(
|
||||
@Serializable
|
||||
data class UpdatePostRequest(
|
||||
val content: String,
|
||||
@SerialName("media_ids") val mediaIds: List<String>? = null
|
||||
@SerialName("media_ids") val mediaIds: List<String>? = null,
|
||||
val visibility: Int? = null // 0=所有人可见, 1=仅自己可见
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -231,8 +231,8 @@ fun MainNavigation(
|
||||
CreatePostScreen(
|
||||
user = user,
|
||||
onClose = { showCreatePost = false },
|
||||
onPost = { content, images ->
|
||||
homeViewModel.createPost(context, content, images)
|
||||
onPost = { content, images, visibility ->
|
||||
homeViewModel.createPost(context, content, images, visibility)
|
||||
},
|
||||
isLoading = isPosting
|
||||
)
|
||||
@@ -243,13 +243,13 @@ fun MainNavigation(
|
||||
EditPostScreen(
|
||||
post = post,
|
||||
onClose = { editingPost = null },
|
||||
onSave = { newContent, existingUrls, newImages ->
|
||||
onSave = { newContent, existingUrls, newImages, visibility ->
|
||||
if (newImages.isEmpty() && existingUrls == post.media.map { it.mediaUrl }) {
|
||||
// 只更新文字
|
||||
homeViewModel.updatePost(post.id, newContent)
|
||||
// 只更新文字和可见性
|
||||
homeViewModel.updatePost(post.id, newContent, null, visibility)
|
||||
} else {
|
||||
// 更新文字和图片
|
||||
homeViewModel.updatePostWithImages(context, post.id, newContent, existingUrls, newImages)
|
||||
// 更新文字、图片和可见性
|
||||
homeViewModel.updatePostWithImages(context, post.id, newContent, existingUrls, newImages, visibility)
|
||||
}
|
||||
editingPost = null
|
||||
},
|
||||
|
||||
@@ -13,7 +13,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.Mood
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -32,7 +34,7 @@ import com.memory.app.ui.theme.*
|
||||
fun CreatePostScreen(
|
||||
user: User?,
|
||||
onClose: () -> Unit,
|
||||
onPost: (String, List<Uri>) -> Unit,
|
||||
onPost: (String, List<Uri>, Int) -> Unit, // 添加 visibility 参数
|
||||
isLoading: Boolean
|
||||
) {
|
||||
// 拦截系统返回手势
|
||||
@@ -43,6 +45,7 @@ fun CreatePostScreen(
|
||||
var content by remember { mutableStateOf("") }
|
||||
var selectedImages by remember { mutableStateOf<List<Uri>>(emptyList()) }
|
||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||
var visibility by remember { mutableStateOf(0) } // 0=所有人可见, 1=仅自己可见
|
||||
|
||||
val imagePicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetMultipleContents()
|
||||
@@ -80,7 +83,7 @@ fun CreatePostScreen(
|
||||
|
||||
// Publish button
|
||||
Button(
|
||||
onClick = { onPost(content, selectedImages) },
|
||||
onClick = { onPost(content, selectedImages, visibility) },
|
||||
enabled = content.isNotBlank() && !isLoading,
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -221,7 +224,10 @@ fun CreatePostScreen(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Image Button
|
||||
IconButton(
|
||||
onClick = { imagePicker.launch("image/*") },
|
||||
@@ -248,6 +254,31 @@ fun CreatePostScreen(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Visibility Selector
|
||||
Surface(
|
||||
onClick = { visibility = if (visibility == 0) 1 else 0 },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (visibility == 0) Icons.Outlined.Public else Icons.Outlined.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = if (visibility == 0) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = if (visibility == 0) "所有人" else "仅自己",
|
||||
fontSize = 12.sp,
|
||||
color = if (visibility == 0) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Character progress indicator
|
||||
|
||||
@@ -13,7 +13,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.Mood
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -32,7 +34,7 @@ import com.memory.app.ui.theme.*
|
||||
fun EditPostScreen(
|
||||
post: Post,
|
||||
onClose: () -> Unit,
|
||||
onSave: (String, List<String>, List<Uri>) -> Unit,
|
||||
onSave: (String, List<String>, List<Uri>, Int) -> Unit, // 添加 visibility 参数
|
||||
isLoading: Boolean
|
||||
) {
|
||||
// 拦截系统返回手势
|
||||
@@ -44,6 +46,7 @@ fun EditPostScreen(
|
||||
var existingImages by remember { mutableStateOf(post.media.map { it.mediaUrl }) }
|
||||
var newImages by remember { mutableStateOf<List<Uri>>(emptyList()) }
|
||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||
var visibility by remember { mutableStateOf(post.visibility) }
|
||||
|
||||
val totalImages = existingImages.size + newImages.size
|
||||
|
||||
@@ -82,7 +85,7 @@ fun EditPostScreen(
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { onSave(content, existingImages, newImages) },
|
||||
onClick = { onSave(content, existingImages, newImages, visibility) },
|
||||
enabled = content.isNotBlank() && !isLoading,
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -246,6 +249,31 @@ fun EditPostScreen(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Visibility Selector
|
||||
Surface(
|
||||
onClick = { visibility = if (visibility == 0) 1 else 0 },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (visibility == 0) Icons.Outlined.Public else Icons.Outlined.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = if (visibility == 0) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = if (visibility == 0) "所有人" else "仅自己",
|
||||
fontSize = 12.sp,
|
||||
color = if (visibility == 0) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,10 +135,10 @@ class HomeViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePost(postId: Long, content: String, mediaIds: List<String>? = null) {
|
||||
fun updatePost(postId: Long, content: String, mediaIds: List<String>? = null, visibility: Int? = null) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, mediaIds))
|
||||
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, mediaIds, visibility))
|
||||
if (response.isSuccessful) {
|
||||
// 刷新获取最新数据
|
||||
loadPosts()
|
||||
@@ -149,7 +149,7 @@ class HomeViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePostWithImages(context: Context, postId: Long, content: String, existingUrls: List<String>, newImages: List<Uri>) {
|
||||
fun updatePostWithImages(context: Context, postId: Long, content: String, existingUrls: List<String>, newImages: List<Uri>, visibility: Int? = null) {
|
||||
viewModelScope.launch {
|
||||
_isPosting.value = true
|
||||
try {
|
||||
@@ -174,7 +174,7 @@ class HomeViewModel : ViewModel() {
|
||||
val allMediaIds = existingUrls + newImageUrls
|
||||
|
||||
// 更新帖子
|
||||
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, allMediaIds))
|
||||
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, allMediaIds, visibility))
|
||||
if (response.isSuccessful) {
|
||||
loadPosts()
|
||||
}
|
||||
@@ -190,7 +190,7 @@ class HomeViewModel : ViewModel() {
|
||||
loadPosts()
|
||||
}
|
||||
|
||||
fun createPost(context: Context, content: String, images: List<Uri>) {
|
||||
fun createPost(context: Context, content: String, images: List<Uri>, visibility: Int = 0) {
|
||||
if (_isPosting.value) return
|
||||
viewModelScope.launch {
|
||||
_isPosting.value = true
|
||||
@@ -216,7 +216,8 @@ class HomeViewModel : ViewModel() {
|
||||
// 创建帖子
|
||||
val request = CreatePostRequest(
|
||||
content = content,
|
||||
mediaIds = imageUrls
|
||||
mediaIds = imageUrls,
|
||||
visibility = visibility
|
||||
)
|
||||
val response = ApiClient.api.createPost(request)
|
||||
if (response.isSuccessful) {
|
||||
|
||||
@@ -146,7 +146,7 @@ update_server_version() {
|
||||
|
||||
echo "🔄 更新服务器版本信息..."
|
||||
|
||||
curl -s -X POST "${API_BASE_URL}/admin/version" \
|
||||
curl -s -X POST "${API_BASE_URL}/version" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"version_code\": ${version_code},
|
||||
|
||||
@@ -127,6 +127,15 @@ func migrate(db *sql.DB) error {
|
||||
// 设置 amos 为超管
|
||||
db.Exec("UPDATE users SET is_superadmin = 1 WHERE username = 'amos'")
|
||||
|
||||
// 迁移:添加 visibility 字段 (0=所有人可见, 1=仅自己可见)
|
||||
db.Exec("ALTER TABLE posts ADD COLUMN visibility INTEGER DEFAULT 0")
|
||||
|
||||
// 创建 visibility 索引
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_posts_visibility ON posts(visibility)")
|
||||
|
||||
// 将 amos 用户的所有帖子设置为仅自己可见
|
||||
db.Exec("UPDATE posts SET visibility = 1 WHERE user_id = (SELECT id FROM users WHERE username = 'amos')")
|
||||
|
||||
// 迁移:创建版本表
|
||||
db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_version (
|
||||
|
||||
@@ -37,7 +37,7 @@ func (h *PostHandler) Create(c *gin.Context) {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
result, err := tx.Exec("INSERT INTO posts (user_id, content) VALUES (?, ?)", userID, req.Content)
|
||||
result, err := tx.Exec("INSERT INTO posts (user_id, content, visibility) VALUES (?, ?, ?)", userID, req.Content, req.Visibility)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create post"})
|
||||
return
|
||||
@@ -79,7 +79,7 @@ func (h *PostHandler) List(c *gin.Context) {
|
||||
if isSuperAdmin != nil && isSuperAdmin.(bool) {
|
||||
// 超管:看到所有帖子
|
||||
rows, err = h.db.Query(`
|
||||
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at,
|
||||
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at, COALESCE(p.visibility, 0),
|
||||
u.id, u.username, u.nickname, u.avatar_url, COALESCE(u.is_superadmin, 0),
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as liked,
|
||||
@@ -90,9 +90,9 @@ func (h *PostHandler) List(c *gin.Context) {
|
||||
LIMIT ? OFFSET ?
|
||||
`, userID, pageSize, offset)
|
||||
} else {
|
||||
// 普通用户:只能看到非超管的帖子
|
||||
// 普通用户:只能看到非超管的帖子 + 可见性为所有人的帖子 + 自己的帖子
|
||||
rows, err = h.db.Query(`
|
||||
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at,
|
||||
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at, COALESCE(p.visibility, 0),
|
||||
u.id, u.username, u.nickname, u.avatar_url, COALESCE(u.is_superadmin, 0),
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as liked,
|
||||
@@ -100,9 +100,10 @@ func (h *PostHandler) List(c *gin.Context) {
|
||||
FROM posts p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE COALESCE(u.is_superadmin, 0) = 0
|
||||
AND (COALESCE(p.visibility, 0) = 0 OR p.user_id = ?)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, userID, pageSize, offset)
|
||||
`, userID, userID, pageSize, offset)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -118,7 +119,7 @@ func (h *PostHandler) List(c *gin.Context) {
|
||||
var liked int
|
||||
var updatedAt sql.NullTime
|
||||
err := rows.Scan(
|
||||
&post.ID, &post.UserID, &post.Content, &post.CreatedAt, &updatedAt,
|
||||
&post.ID, &post.UserID, &post.Content, &post.CreatedAt, &updatedAt, &post.Visibility,
|
||||
&user.ID, &user.Username, &user.Nickname, &user.AvatarURL, &user.IsSuperAdmin,
|
||||
&post.LikeCount, &liked, &post.CommentCount,
|
||||
)
|
||||
@@ -152,7 +153,7 @@ func (h *PostHandler) Get(c *gin.Context) {
|
||||
|
||||
var updatedAt sql.NullTime
|
||||
err := h.db.QueryRow(`
|
||||
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at,
|
||||
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at, COALESCE(p.visibility, 0),
|
||||
u.id, u.username, u.nickname, u.avatar_url, COALESCE(u.is_superadmin, 0),
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as liked,
|
||||
@@ -161,7 +162,7 @@ func (h *PostHandler) Get(c *gin.Context) {
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = ?
|
||||
`, userID, postID).Scan(
|
||||
&post.ID, &post.UserID, &post.Content, &post.CreatedAt, &updatedAt,
|
||||
&post.ID, &post.UserID, &post.Content, &post.CreatedAt, &updatedAt, &post.Visibility,
|
||||
&user.ID, &user.Username, &user.Nickname, &user.AvatarURL, &user.IsSuperAdmin,
|
||||
&post.LikeCount, &liked, &post.CommentCount,
|
||||
)
|
||||
@@ -218,7 +219,11 @@ func (h *PostHandler) Update(c *gin.Context) {
|
||||
defer tx.Rollback()
|
||||
|
||||
// 更新帖子内容
|
||||
_, err = tx.Exec("UPDATE posts SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", req.Content, postID)
|
||||
if req.Visibility != nil {
|
||||
_, err = tx.Exec("UPDATE posts SET content = ?, visibility = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", req.Content, *req.Visibility, postID)
|
||||
} else {
|
||||
_, err = tx.Exec("UPDATE posts SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", req.Content, postID)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update post"})
|
||||
return
|
||||
|
||||
@@ -18,6 +18,7 @@ type Post struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Visibility int `json:"visibility"` // 0=所有人可见, 1=仅自己可见
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
@@ -85,13 +86,15 @@ type LoginResponse struct {
|
||||
}
|
||||
|
||||
type CreatePostRequest struct {
|
||||
Content string `json:"content" binding:"required,max=1000"`
|
||||
MediaIDs []string `json:"media_ids"`
|
||||
Content string `json:"content" binding:"required,max=1000"`
|
||||
MediaIDs []string `json:"media_ids"`
|
||||
Visibility int `json:"visibility"` // 0=所有人可见, 1=仅自己可见
|
||||
}
|
||||
|
||||
type UpdatePostRequest struct {
|
||||
Content string `json:"content" binding:"required,max=1000"`
|
||||
MediaIDs []string `json:"media_ids"`
|
||||
Content string `json:"content" binding:"required,max=1000"`
|
||||
MediaIDs []string `json:"media_ids"`
|
||||
Visibility *int `json:"visibility"` // 0=所有人可见, 1=仅自己可见
|
||||
}
|
||||
|
||||
type CreateCommentRequest struct {
|
||||
|
||||
@@ -50,6 +50,7 @@ func Setup(db *sql.DB, cfg *config.Config) *gin.Engine {
|
||||
r.POST("/api/auth/register", authHandler.Register)
|
||||
r.POST("/api/auth/login", authHandler.Login)
|
||||
r.GET("/api/version", versionHandler.GetLatestVersion)
|
||||
r.POST("/api/version", versionHandler.SetVersion) // 内部使用,无需认证
|
||||
|
||||
// 需要认证的接口
|
||||
auth := r.Group("/api")
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user