chore: publish native ui polish release v2.5.3

This commit is contained in:
kris
2026-03-29 20:43:16 +08:00
parent ef630f3572
commit 3724b3b444
15 changed files with 173 additions and 113 deletions

View File

@@ -90,7 +90,7 @@ Android APK
- 已生成 Android debug APK`android/app/build/outputs/apk/debug/app-debug.apk`
- 已生成 Android signed release APK`android/app/build/outputs/apk/release/app-release.apk`
- `npm run apk:release` 还会额外产出带版本号的文件:`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk`
- 当前最新 release 构建版本:`2.5.2``versionCode=15`
- 当前最新 release 构建版本:`2.5.3``versionCode=16`
- 当前 APK 已切到原生 Android 客户端:`MainActivity + BossApiClient + 原生 XML 布局`
- 当前原生活动页已经覆盖会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、技能、运维中心、关于
- 当前原生一级体验已回退到微信式交互:`会话 / 设备 / 我的` 固定底部 tab会话首页是简单聊天列表`主 Agent / 审计对话` 以普通置顶会话样式排在最前;项目详情页是聊天优先,只保留 `项目目标 / 版本记录` 两个轻入口
@@ -112,6 +112,7 @@ Android APK
- `2.5.0` 已补齐聊天附件主链:原生聊天框左侧 `+` 会打开底部抽屉,支持图片 / 视频 / 文件发送;默认走服务器文件存储,`我的 > 附件与存储` 可切到阿里 OSS 私有桶;附件消息已支持下载 / 打开、手动分析、自动分析状态,以及带 task token 的主 Agent 附件分析链接
- `2.5.1` 继续收口微信式原生 UI聊天页普通态顶部已隐藏刷新按钮只保留右上角“信息”发起群聊页顶部说明和选择区已压成更轻的会话式密度候选线程继续复用微信式会话卡片
- `2.5.2` 继续补齐深层原生页:`项目目标 / 版本迭代记录 / 会话信息 / 群资料` 已进一步向设计图收口;附件消息卡片的分析状态和动作文案也压成了更轻的微信式层级
- `2.5.3` 继续压缩原生聊天与建群页面:`发起群聊` 页已收成轻量头部 + hint pill + 一层操作区;聊天页里自己发出的消息顶部元信息已收成只显示时间,不再重复 `你 · 时间`
## 本地启动

View File

@@ -36,8 +36,8 @@ android {
applicationId "com.hyzq.boss"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 15
versionName "2.5.2"
versionCode 16
versionName "2.5.3"
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -912,10 +912,7 @@ public final class BossUi {
wrapper.setLayoutParams(wrapperParams);
TextView metaView = new TextView(context);
String metaText = senderLabel;
if (!TextUtils.isEmpty(meta)) {
metaText = metaText + " · " + meta;
}
String metaText = buildMessageMetaText(senderLabel, meta, outgoing);
metaView.setText(metaText);
metaView.setTextSize(11);
metaView.setTextColor(context.getColor(R.color.boss_text_soft));
@@ -1049,10 +1046,7 @@ public final class BossUi {
wrapper.setLayoutParams(wrapperParams);
TextView metaView = new TextView(context);
String metaText = senderLabel;
if (!TextUtils.isEmpty(meta)) {
metaText = metaText + " · " + meta;
}
String metaText = buildMessageMetaText(senderLabel, meta, outgoing);
metaView.setText(metaText);
metaView.setTextSize(11);
metaView.setTextColor(context.getColor(R.color.boss_text_soft));
@@ -1061,6 +1055,23 @@ public final class BossUi {
return wrapper;
}
private static String buildMessageMetaText(
String senderLabel,
@Nullable String meta,
boolean outgoing
) {
if (outgoing && !TextUtils.isEmpty(meta)) {
return meta;
}
if (TextUtils.isEmpty(meta)) {
return senderLabel;
}
if (TextUtils.isEmpty(senderLabel)) {
return meta;
}
return senderLabel + " · " + meta;
}
private static TextView buildAttachmentPrimaryText(Context context, String text) {
TextView primary = new TextView(context);
primary.setText(TextUtils.isEmpty(text) ? "未命名附件" : text);

View File

@@ -2,8 +2,11 @@ package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -85,15 +88,13 @@ public class GroupCreateActivity extends BossScreenActivity {
? sourceProjectName
: threadMeta.optString("threadDisplayName", sourceProjectName == null ? "当前会话" : sourceProjectName);
}
for (SummaryCardSpec cardSpec : buildHeaderCardSpecs(
appendContent(buildHeaderView(
hasSourceProject(),
sourceProjectId,
sourceProjectName,
threadMeta,
participants
)) {
appendContent(BossUi.buildCard(this, cardSpec.title, cardSpec.body, cardSpec.meta));
}
));
if (rebuildCandidates) {
List<JSONObject> selectableConversations = collectSelectableConversationItems(conversationsPayload, sourceProjectId);
@@ -121,16 +122,10 @@ public class GroupCreateActivity extends BossScreenActivity {
lastCandidateProjectIds.addAll(nextCandidateProjectIds);
}
SummaryCardSpec selectionCard = buildSelectionCardSpec(
candidates.size(),
selectedProjectIds.size(),
hasSourceProject()
);
appendContent(BossUi.buildCard(
appendContent(buildSectionLabel("选择其他线程"));
appendContent(BossUi.buildHintPill(
this,
selectionCard.title,
selectionCard.body,
selectionCard.meta
buildSelectionHintText(candidates.size(), selectedProjectIds.size(), hasSourceProject())
));
candidateListLayout = new LinearLayout(this);
@@ -145,16 +140,53 @@ public class GroupCreateActivity extends BossScreenActivity {
createButton = BossUi.buildPrimaryButton(this, "创建群聊");
createButton.setOnClickListener(v -> createGroupChat());
appendContent(createButton);
Button cancelButton = BossUi.buildSecondaryButton(this, "取消");
cancelButton.setOnClickListener(v -> finish());
appendContent(cancelButton);
appendContent(BossUi.buildInlineActionRow(this, cancelButton, createButton));
setRefreshing(false);
updateCreateButtonState();
}
private View buildHeaderView(
boolean hasSourceProject,
@Nullable String sourceProjectId,
@Nullable String sourceProjectName,
@Nullable JSONObject threadMeta,
@Nullable JSONArray participants
) {
if (hasSourceProject) {
return BossUi.buildSimpleProfileHeader(
this,
TextUtils.isEmpty(sourceProjectName) ? "当前会话" : sourceProjectName,
"从当前会话发起群聊",
buildSourceHeaderDetail(sourceProjectId, threadMeta, participants)
);
}
return BossUi.buildSimpleProfileHeader(
this,
"发起新群聊",
"从会话列表直接建群",
"至少选择 2 个线程后创建新群"
);
}
private TextView buildSectionLabel(String text) {
TextView label = new TextView(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = BossUi.dp(this, 16);
params.rightMargin = BossUi.dp(this, 16);
params.bottomMargin = BossUi.dp(this, 6);
label.setLayoutParams(params);
label.setText(text);
label.setTextSize(13);
label.setTextColor(getColor(R.color.boss_text_muted));
return label;
}
static List<JSONObject> collectSelectableConversationItems(@Nullable JSONObject conversationsPayload, String sourceProjectId) {
List<JSONObject> result = new ArrayList<>();
JSONArray conversations = conversationsPayload == null ? null : conversationsPayload.optJSONArray("conversations");
@@ -192,65 +224,39 @@ public class GroupCreateActivity extends BossScreenActivity {
);
}
static List<SummaryCardSpec> buildHeaderCardSpecs(
boolean hasSourceProject,
static String buildSourceHeaderDetail(
@Nullable String sourceProjectId,
@Nullable String sourceProjectName,
@Nullable JSONObject threadMeta,
@Nullable JSONArray participants
) {
List<SummaryCardSpec> cards = new ArrayList<>(1);
if (hasSourceProject) {
String resolvedSourceProjectName = sourceProjectName == null || sourceProjectName.isEmpty()
? "当前会话"
: sourceProjectName;
cards.add(new SummaryCardSpec(
resolvedSourceProjectName,
buildSourceBody(sourceProjectId, threadMeta, participants),
"新群会单独创建,原会话保留"
));
return cards;
}
cards.add(new SummaryCardSpec(
"从会话中发起群聊",
"至少选择 2 个线程,新群会单独创建。",
"原有单线程会话会保留"
));
return cards;
return buildSourceBody(sourceProjectId, threadMeta, participants);
}
static SummaryCardSpec buildSelectionCardSpec(
static String buildSelectionHintText(
int candidateCount,
int selectedCount,
boolean hasSourceProject
) {
if (candidateCount <= 0) {
return new SummaryCardSpec(
"选择其他线程",
"当前没有可加入的其他线程",
"候选 0 个 · 已选 0 个"
);
return "当前没有可加入的其他线程";
}
if (selectedCount <= 0) {
return new SummaryCardSpec(
"选择其他线程",
hasSourceProject ? "至少选择 1 个其他线程" : "至少选择 2 个线程",
"候选 " + candidateCount + " 个 · 已选 0 个"
);
return hasSourceProject ? "至少选择 1 个其他线程" : "至少选择 2 个线程";
}
return new SummaryCardSpec(
"选择其他线程",
"继续点选可调整成员",
"候选 " + candidateCount + " 个 · 已选 " + selectedCount + ""
);
return "已选 " + selectedCount + " 个线程";
}
private LinearLayout buildCandidateRow(CandidateConversation candidate) {
return BossUi.buildConversationRow(
LinearLayout row = BossUi.buildConversationRow(
this,
toCandidateConversationRow(candidate.sourceItem, selectedProjectIds.contains(candidate.projectId)),
v -> toggleSelection(candidate.projectId)
);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) row.getLayoutParams();
params.bottomMargin = BossUi.dp(this, 8);
row.setLayoutParams(params);
row.setPadding(BossUi.dp(this, 12), BossUi.dp(this, 12), BossUi.dp(this, 12), BossUi.dp(this, 12));
return row;
}
private void toggleSelection(String projectId) {
@@ -399,7 +405,7 @@ public class GroupCreateActivity extends BossScreenActivity {
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
String resolvedFolderName = folderName.isEmpty() ? "未命名文件夹" : folderName;
String resolvedThreadId = threadId == null || threadId.isEmpty() ? "未命名线程" : threadId;
return "来源线程 " + resolvedThreadId
return resolvedThreadId
+ " · "
+ resolvedFolderName
+ " · "
@@ -407,18 +413,6 @@ public class GroupCreateActivity extends BossScreenActivity {
+ " 个参与线程";
}
static final class SummaryCardSpec {
final String title;
final String body;
final String meta;
SummaryCardSpec(String title, String body, String meta) {
this.title = title;
this.body = body;
this.meta = meta;
}
}
private static final class CandidateConversation {
private final String projectId;
private final JSONObject sourceItem;

View File

@@ -14,7 +14,7 @@ import static org.junit.Assert.assertTrue;
public class GroupCreateActivityTest {
@Test
public void buildHeaderCardSpecs_collapsesSourceFlowIntoSingleCompactCard() {
public void buildSourceHeaderDetail_usesCompactWechatSummary() {
JSONObject threadMeta = new StubJSONObject()
.withString("threadDisplayName", "北区试产线回归")
.withString("threadId", "thread-7")
@@ -25,27 +25,20 @@ public class GroupCreateActivityTest {
new StubJSONObject()
);
List<GroupCreateActivity.SummaryCardSpec> cards = GroupCreateActivity.buildHeaderCardSpecs(
true,
String detail = GroupCreateActivity.buildSourceHeaderDetail(
"source-1",
"北区试产线回归",
threadMeta,
participants
);
assertEquals(1, cards.size());
assertEquals("北区试产线回归", cards.get(0).title);
assertEquals("来源线程 thread-7 · Mac Studio · 3 个参与线程", cards.get(0).body);
assertEquals("新群会单独创建,原会话保留", cards.get(0).meta);
assertEquals("thread-7 · Mac Studio · 3 个参与线程", detail);
}
@Test
public void buildSelectionCardSpec_usesCompactWechatStyleHint() {
GroupCreateActivity.SummaryCardSpec card = GroupCreateActivity.buildSelectionCardSpec(5, 0, true);
assertEquals("选择其他线程", card.title);
assertEquals("至少选择 1 个其他线程", card.body);
assertEquals("候选 5 个 · 已选 0 个", card.meta);
public void buildSelectionHintText_usesCompactWechatStyleHint() {
assertEquals("至少选择 1 个其他线程", GroupCreateActivity.buildSelectionHintText(5, 0, true));
assertEquals("已选 2 个线程", GroupCreateActivity.buildSelectionHintText(5, 2, true));
assertEquals("当前没有可加入的其他线程", GroupCreateActivity.buildSelectionHintText(0, 0, true));
}
@Test

View File

@@ -40,11 +40,10 @@ public class GroupCreateActivityUiTest {
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "来源线程 thread-7 · Mac Studio · 3 个参与线程"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "新群会单独创建,原会话保留"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "从当前会话发起群聊"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "thread-7 · Mac Studio · 3 个参与线程"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "继续点选可调整成员"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "候选 2 个 · 已选 2 个"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "已选 2 个线程"));
}
@Test
@@ -77,8 +76,32 @@ public class GroupCreateActivityUiTest {
);
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content.getChildAt(1), "至少选择 1 个其他线程"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "候选 2 个 · 已选 0 个"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "至少选择 1 个其他线程"));
}
@Test
public void renderCreatePageUsesInlineActionRowInsteadOfStackedButtons() throws Exception {
Intent intent = new Intent()
.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_ID, "source-1")
.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_NAME, "北区试产线回归");
TestGroupCreateActivity activity = Robolectric
.buildActivity(TestGroupCreateActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderCreatePage",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildConversationsPayload()),
ReflectionHelpers.ClassParameter.from(boolean.class, true)
);
LinearLayout content = activity.findViewById(R.id.screen_content);
View lastChild = content.getChildAt(content.getChildCount() - 1);
assertTrue(lastChild instanceof LinearLayout);
assertTrue(viewTreeContainsText(lastChild, "取消"));
assertTrue(viewTreeContainsText(lastChild, "创建群聊"));
}
private static JSONObject buildParticipantsPayload() throws Exception {

View File

@@ -190,6 +190,42 @@ public class ProjectDetailActivityUiTest {
assertFalse(viewTreeContainsText(attachmentView, "已分析"));
}
@Test
public void outgoingAttachmentMetaPrefersTimeOnly() throws Exception {
Intent intent = new Intent()
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
TestProjectDetailActivity activity = Robolectric
.buildActivity(TestProjectDetailActivity.class, intent)
.setup()
.get();
JSONObject attachment = new JSONObject()
.put("attachmentId", "att-meta")
.put("fileName", "现场照片.jpg")
.put("mimeType", "image/jpeg")
.put("attachmentKind", "image")
.put("analysisState", "queued_auto")
.put("fileSizeBytes", 1024);
JSONObject message = new JSONObject()
.put("id", "msg-meta")
.put("kind", "attachment")
.put("body", "已发送附件")
.put("attachments", new JSONArray().put(attachment));
View attachmentView = ReflectionHelpers.callInstanceMethod(
activity,
"buildAttachmentMessageView",
ReflectionHelpers.ClassParameter.from(JSONObject.class, message),
ReflectionHelpers.ClassParameter.from(String.class, ""),
ReflectionHelpers.ClassParameter.from(String.class, "09:26"),
ReflectionHelpers.ClassParameter.from(boolean.class, true)
);
assertTrue(viewTreeContainsText(attachmentView, "09:26"));
assertFalse(viewTreeContainsText(attachmentView, "你 · 09:26"));
}
private static View buildBoundMessageView(TestProjectDetailActivity activity, String messageId, String body) {
TextView messageView = new TextView(activity);
messageView.setText(body);

View File

@@ -153,7 +153,7 @@
- 邮件:`Postfix + Dovecot`
- Android`AppCompatActivity + 原生 XML 布局 + HttpURLConnection`
- 原生登录恢复:`SharedPreferences + restore token`
- 当前最新原生 APK`2.5.2``versionCode=15`
- 当前最新原生 APK`2.5.3``versionCode=16`
当前不要误判成已经用了:

View File

@@ -116,7 +116,7 @@ cd /Users/kris/code/boss
- 当前已生成 Android debug APK`android/app/build/outputs/apk/debug/app-debug.apk`
- 当前已生成 Android signed release APK`android/app/build/outputs/apk/release/app-release.apk`
- 当前 release 构建还会额外生成带版本号的 APK`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk`
- 当前最新 release 构建版本:`2.5.2``versionCode=15`
- 当前最新 release 构建版本:`2.5.3``versionCode=16`
- 当前 release keystore 位于本机 `android/keystores/boss-release.keystore`,签名参数位于 `android/signing/release-signing.properties`
- `2.0.1` 已在本机连接的华为真机上复核通过,修复了 `Theme.SplashScreen` 导致的 `AppCompatActivity` 启动闪退
- `2.1.0` 已把 Web 一级页和主要二级页全部补成原生活动页:`MainActivity / ProjectDetailActivity / ProjectGoalsActivity / ProjectVersionsActivity / ProjectForwardActivity / ThreadDetailActivity / DeviceDetailActivity / DeviceEnrollmentActivity / SkillInventoryActivity / SecurityActivity / SettingsActivity / AiAccountsActivity / OpsCenterActivity / AboutActivity`
@@ -132,6 +132,8 @@ cd /Users/kris/code/boss
- `2.5.1` 已压缩“发起群聊”页首信息密度:来源会话场景只保留一张紧凑摘要卡,选择区改成更短的微信式提示,同时保留会话卡片式候选列表
- `2.5.2` 已继续回退深层原生页:`会话信息 / 群资料` 改为轻量头部信息 + 菜单式入口 + 线程列表;`项目目标 / 版本迭代记录` 也已按设计图改成轻卡片结构,不再使用厚按钮和说明块
- `2.5.2` 已压缩附件消息卡片的状态层级:`待分析` 收成 `可分析``让 AI 分析` 收成 `AI 分析`,有摘要时不再重复显示 `已分析`
- `2.5.3` 已继续压缩“发起群聊”页:来源会话摘要改成轻量 profile header选择区改成 `选择其他线程 + hint pill`,底部按钮收成同层操作区,候选会话卡片纵向间距进一步压缩
- `2.5.3` 已把聊天页里自己发出的消息顶部元信息收成只显示时间,不再重复 `你 · 时间`,文本消息、附件卡片和聊天记录卡片都会共用这条规则
- 当前附件分析任务已带受控 `task token` 下载链接和文本摘录:本地开发环境会跟随请求 origin 生成链接,生产环境默认走 `https://boss.hyzq.net`
- `2.5.x` 当前已补上会话首页独立建群入口:可以不从单线程聊天内部出发,直接在会话首页右上角 `+` 建立新群聊;同时已把多个原生自定义 top bar 页面统一纳入状态栏安全区处理

View File

@@ -1,11 +1,11 @@
{
"artifactType": "aab",
"fileName": "boss-android-v2.5.2-release.aab",
"urlPath": "/downloads/boss-android-v2.5.2-release.aab",
"sizeBytes": 2912579,
"updatedAt": "2026-03-29T12:26:43Z",
"sha256": "dca2dc0a080bba282505bacf7e8a37e5aca2e695a657b46df0e184c1ff077d6e",
"versionName": "2.5.2",
"versionCode": 15,
"fileName": "boss-android-v2.5.3-release.aab",
"urlPath": "/downloads/boss-android-v2.5.3-release.aab",
"sizeBytes": 2912097,
"updatedAt": "2026-03-29T12:41:56Z",
"sha256": "12b520dba9a9fa3075c374910a595943d3f2a0b9f70a813bdfe8d1546e380d3c",
"versionName": "2.5.3",
"versionCode": 16,
"buildFlavor": "release"
}

View File

@@ -1,10 +1,10 @@
{
"fileName": "boss-android-v2.5.2-release.apk",
"fileName": "boss-android-v2.5.3-release.apk",
"urlPath": "/api/v1/user/ota/package",
"sizeBytes": 3089319,
"updatedAt": "2026-03-29T12:26:31Z",
"sha256": "a27be7abe260c42dcd357206adf292f48bdd33bb14e343c25ee38faeebefd97f",
"versionName": "2.5.2",
"versionCode": 15,
"sizeBytes": 3088834,
"updatedAt": "2026-03-29T12:41:51Z",
"sha256": "dd44b6e1228966fbd6e83ab53936393024a76e17f7fe996e15442f4f105a435a",
"versionName": "2.5.3",
"versionCode": 16,
"buildFlavor": "release"
}

Binary file not shown.

Binary file not shown.