fix: restore safe top actions and home group chat entry
This commit is contained in:
@@ -92,6 +92,10 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/group-chat", payload == null ? new JSONObject() : payload);
|
||||
}
|
||||
|
||||
public ApiResponse createStandaloneGroupChat(JSONObject payload) throws IOException, JSONException {
|
||||
return requestWithRestore("POST", "/api/v1/group-chats", payload == null ? new JSONObject() : payload);
|
||||
}
|
||||
|
||||
public ApiResponse getConversationParticipants(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/participants", null);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -20,6 +21,7 @@ public abstract class BossScreenActivity extends AppCompatActivity {
|
||||
protected Button backButton;
|
||||
protected Button refreshButton;
|
||||
protected Button headerActionButton;
|
||||
protected View topBarView;
|
||||
protected TextView titleView;
|
||||
protected TextView subtitleView;
|
||||
protected SwipeRefreshLayout refreshLayout;
|
||||
@@ -34,11 +36,14 @@ public abstract class BossScreenActivity extends AppCompatActivity {
|
||||
backButton = findViewById(R.id.screen_back_button);
|
||||
refreshButton = findViewById(R.id.screen_refresh_button);
|
||||
headerActionButton = findViewById(R.id.screen_header_action);
|
||||
topBarView = findViewById(R.id.screen_top_bar);
|
||||
titleView = findViewById(R.id.screen_title);
|
||||
subtitleView = findViewById(R.id.screen_subtitle);
|
||||
refreshLayout = findViewById(R.id.screen_refresh_layout);
|
||||
contentLayout = findViewById(R.id.screen_content);
|
||||
|
||||
BossWindowInsets.applyStatusBarInset(topBarView);
|
||||
|
||||
backButton.setOnClickListener(v -> finish());
|
||||
refreshButton.setOnClickListener(v -> reload());
|
||||
refreshLayout.setOnRefreshListener(this::reload);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
public final class BossWindowInsets {
|
||||
private BossWindowInsets() {}
|
||||
|
||||
public static void applyStatusBarInset(View view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
final int initialLeft = view.getPaddingLeft();
|
||||
final int initialTop = view.getPaddingTop();
|
||||
final int initialRight = view.getPaddingRight();
|
||||
final int initialBottom = view.getPaddingBottom();
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, (target, insets) -> {
|
||||
Insets statusInsets = insets.getInsets(
|
||||
WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()
|
||||
);
|
||||
target.setPadding(
|
||||
initialLeft,
|
||||
initialTop + statusInsets.top,
|
||||
initialRight,
|
||||
initialBottom
|
||||
);
|
||||
return insets;
|
||||
});
|
||||
|
||||
if (ViewCompat.isAttachedToWindow(view)) {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
return;
|
||||
}
|
||||
|
||||
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
v.removeOnAttachStateChangeListener(this);
|
||||
ViewCompat.requestApplyInsets(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -42,24 +42,26 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
sourceProjectId = getIntent().getStringExtra(EXTRA_SOURCE_PROJECT_ID);
|
||||
sourceProjectName = getIntent().getStringExtra(EXTRA_SOURCE_PROJECT_NAME);
|
||||
configureScreen("发起群聊", sourceProjectName == null ? "从当前会话出发" : sourceProjectName);
|
||||
configureScreen(
|
||||
"发起群聊",
|
||||
hasSourceProject() ? (sourceProjectName == null ? "从当前会话出发" : sourceProjectName) : "从会话列表直接建群"
|
||||
);
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (sourceProjectId == null || sourceProjectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(sourceProjectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
BossApiClient.ApiResponse conversationsResponse = apiClient.getConversations();
|
||||
if (!conversationsResponse.ok()) throw new IllegalStateException(conversationsResponse.message());
|
||||
if (!hasSourceProject()) {
|
||||
runOnUiThread(() -> renderCreatePage(null, conversationsResponse.json, true));
|
||||
return;
|
||||
}
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(sourceProjectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
runOnUiThread(() -> renderCreatePage(participantsResponse.json, conversationsResponse.json, true));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -78,23 +80,32 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
JSONObject threadMeta = participantsPayload.optJSONObject("threadMeta");
|
||||
JSONArray participants = participantsPayload.optJSONArray("participants");
|
||||
sourceFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
sourceProjectName = threadMeta == null
|
||||
? sourceProjectName
|
||||
: threadMeta.optString("threadDisplayName", sourceProjectName == null ? "当前会话" : sourceProjectName);
|
||||
if (hasSourceProject()) {
|
||||
sourceProjectName = threadMeta == null
|
||||
? sourceProjectName
|
||||
: threadMeta.optString("threadDisplayName", sourceProjectName == null ? "当前会话" : sourceProjectName);
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"新建独立群聊",
|
||||
"群聊不是升级原会话,而是以当前会话为源,新建一个独立线程。",
|
||||
buildSourceMeta(threadMeta, participants)
|
||||
));
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"新建独立群聊",
|
||||
"群聊不是升级原会话,而是以当前会话为源,新建一个独立线程。",
|
||||
buildSourceMeta(threadMeta, participants)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
sourceProjectName,
|
||||
buildSourceBody(threadMeta, participants),
|
||||
sourceProjectId + (sourceFolderName.isEmpty() ? "" : " · " + sourceFolderName)
|
||||
));
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
sourceProjectName,
|
||||
buildSourceBody(threadMeta, participants),
|
||||
sourceProjectId + (sourceFolderName.isEmpty() ? "" : " · " + sourceFolderName)
|
||||
));
|
||||
} else {
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"从会话首页发起群聊",
|
||||
"你可以直接把任意线程拉进一个新的独立群聊,原来的单线程会话会保留不变。",
|
||||
"至少选择 2 个线程"
|
||||
));
|
||||
}
|
||||
|
||||
if (rebuildCandidates) {
|
||||
List<JSONObject> selectableConversations = collectSelectableConversationItems(conversationsPayload, sourceProjectId);
|
||||
@@ -119,7 +130,8 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
selectedProjectIds.addAll(reconcileSelectedProjectIds(
|
||||
currentSelectedProjectIds,
|
||||
lastCandidateProjectIds,
|
||||
nextCandidateProjectIds
|
||||
nextCandidateProjectIds,
|
||||
hasSourceProject()
|
||||
));
|
||||
lastCandidateProjectIds.clear();
|
||||
lastCandidateProjectIds.addAll(nextCandidateProjectIds);
|
||||
@@ -164,11 +176,14 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
if (conversations == null) {
|
||||
return result;
|
||||
}
|
||||
boolean hasSourceProject = sourceProjectId != null && !sourceProjectId.isEmpty();
|
||||
for (int i = 0; i < conversations.length(); i++) {
|
||||
JSONObject item = conversations.optJSONObject(i);
|
||||
if (item == null) continue;
|
||||
String projectId = item.optString("projectId", "");
|
||||
if (projectId.isEmpty() || sourceProjectId.equals(projectId) || item.optBoolean("isGroup", false)) {
|
||||
if (projectId.isEmpty()
|
||||
|| (hasSourceProject && sourceProjectId.equals(projectId))
|
||||
|| item.optBoolean("isGroup", false)) {
|
||||
continue;
|
||||
}
|
||||
result.add(item);
|
||||
@@ -214,7 +229,7 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
private void updateCreateButtonState() {
|
||||
if (createButton != null) {
|
||||
boolean refreshing = refreshLayout != null && refreshLayout.isRefreshing();
|
||||
createButton.setEnabled(canCreateGroupChat(refreshing, creatingGroupChat, selectedProjectIds));
|
||||
createButton.setEnabled(canCreateGroupChat(refreshing, creatingGroupChat, selectedProjectIds, hasSourceProject()));
|
||||
createButton.setText(creatingGroupChat ? "创建中..." : "创建群聊");
|
||||
}
|
||||
}
|
||||
@@ -240,7 +255,9 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
memberProjectIds.put(projectId);
|
||||
}
|
||||
payload.put("memberProjectIds", memberProjectIds);
|
||||
BossApiClient.ApiResponse response = apiClient.createGroupChat(sourceProjectId, payload);
|
||||
BossApiClient.ApiResponse response = hasSourceProject()
|
||||
? apiClient.createGroupChat(sourceProjectId, payload)
|
||||
: apiClient.createStandaloneGroupChat(payload);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
JSONObject project = response.json.optJSONObject("project");
|
||||
if (project == null) throw new IllegalStateException("GROUP_CHAT_PROJECT_MISSING");
|
||||
@@ -274,18 +291,33 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
static boolean canCreateGroupChat(
|
||||
boolean refreshing,
|
||||
boolean creatingGroupChat,
|
||||
@Nullable Set<String> selectedProjectIds
|
||||
@Nullable Set<String> selectedProjectIds,
|
||||
boolean hasSourceProject
|
||||
) {
|
||||
return !refreshing
|
||||
&& !creatingGroupChat
|
||||
&& selectedProjectIds != null
|
||||
&& !selectedProjectIds.isEmpty();
|
||||
&& selectedProjectIds.size() >= (hasSourceProject ? 1 : 2);
|
||||
}
|
||||
|
||||
static Set<String> reconcileSelectedProjectIds(
|
||||
@Nullable Set<String> currentSelectedProjectIds,
|
||||
@Nullable Set<String> previousCandidateProjectIds,
|
||||
@Nullable Set<String> nextCandidateProjectIds
|
||||
) {
|
||||
return reconcileSelectedProjectIds(
|
||||
currentSelectedProjectIds,
|
||||
previousCandidateProjectIds,
|
||||
nextCandidateProjectIds,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static Set<String> reconcileSelectedProjectIds(
|
||||
@Nullable Set<String> currentSelectedProjectIds,
|
||||
@Nullable Set<String> previousCandidateProjectIds,
|
||||
@Nullable Set<String> nextCandidateProjectIds,
|
||||
boolean defaultSelectAll
|
||||
) {
|
||||
Set<String> reconciled = new LinkedHashSet<>();
|
||||
if (nextCandidateProjectIds == null || nextCandidateProjectIds.isEmpty()) {
|
||||
@@ -294,7 +326,9 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
if (previousCandidateProjectIds == null
|
||||
|| previousCandidateProjectIds.isEmpty()
|
||||
|| !previousCandidateProjectIds.equals(nextCandidateProjectIds)) {
|
||||
reconciled.addAll(nextCandidateProjectIds);
|
||||
if (defaultSelectAll) {
|
||||
reconciled.addAll(nextCandidateProjectIds);
|
||||
}
|
||||
return reconciled;
|
||||
}
|
||||
if (currentSelectedProjectIds == null || currentSelectedProjectIds.isEmpty()) {
|
||||
@@ -308,6 +342,10 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
return reconciled;
|
||||
}
|
||||
|
||||
private boolean hasSourceProject() {
|
||||
return sourceProjectId != null && !sourceProjectId.isEmpty();
|
||||
}
|
||||
|
||||
private String buildSourceMeta(@Nullable JSONObject threadMeta, @Nullable JSONArray participants) {
|
||||
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
int count = participants == null ? 0 : participants.length();
|
||||
|
||||
@@ -32,6 +32,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private View loginPanel;
|
||||
private View contentPanel;
|
||||
private View loginShell;
|
||||
private View mainTopBar;
|
||||
private TextView loginTitle;
|
||||
private TextView loginHint;
|
||||
private Button loginButton;
|
||||
@@ -110,6 +112,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void bindViews() {
|
||||
loginPanel = findViewById(R.id.login_panel);
|
||||
contentPanel = findViewById(R.id.content_panel);
|
||||
loginShell = findViewById(R.id.login_shell);
|
||||
mainTopBar = findViewById(R.id.main_top_bar);
|
||||
loginTitle = findViewById(R.id.login_title);
|
||||
loginHint = findViewById(R.id.login_hint);
|
||||
loginButton = findViewById(R.id.login_button);
|
||||
@@ -131,6 +135,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
loginTitle.setText(WechatSurfaceMapper.loginTitle());
|
||||
loginHint.setText(WechatSurfaceMapper.loginHintText());
|
||||
loginButton.setText(WechatSurfaceMapper.loginButtonLabel());
|
||||
BossWindowInsets.applyStatusBarInset(loginShell);
|
||||
BossWindowInsets.applyStatusBarInset(mainTopBar);
|
||||
}
|
||||
|
||||
private void bindActions() {
|
||||
@@ -368,18 +374,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
switch (activeTab) {
|
||||
case "devices":
|
||||
updateHeader("设备", "这里管理已接入设备与账号状态。");
|
||||
configureTopAction("+添加", true);
|
||||
configureTopAction(WechatSurfaceMapper.rootTopAction(activeTab, false));
|
||||
renderDevicesRoot();
|
||||
break;
|
||||
case "me":
|
||||
updateHeader("我的", "");
|
||||
configureTopAction("刷新", false);
|
||||
configureTopAction(WechatSurfaceMapper.rootTopAction(activeTab, false));
|
||||
renderMeRoot();
|
||||
break;
|
||||
case "conversations":
|
||||
default:
|
||||
updateHeader("会话", WechatSurfaceMapper.conversationsHeaderSubtitle());
|
||||
configureTopAction("刷新", false);
|
||||
configureTopAction(WechatSurfaceMapper.rootTopAction(activeTab, false));
|
||||
renderConversationsRoot();
|
||||
break;
|
||||
}
|
||||
@@ -402,27 +408,28 @@ public class MainActivity extends AppCompatActivity {
|
||||
button.setTextColor(getColor(active ? R.color.boss_green : R.color.boss_text_muted));
|
||||
}
|
||||
|
||||
private void configureTopAction(String label, boolean primaryStyle) {
|
||||
refreshButton.setText(label);
|
||||
refreshButton.setBackgroundResource(primaryStyle ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
|
||||
refreshButton.setTextColor(getColor(primaryStyle ? R.color.boss_surface : R.color.boss_green));
|
||||
private void configureTopAction(WechatSurfaceMapper.RootTopAction action) {
|
||||
refreshButton.setText(action.label);
|
||||
refreshButton.setBackgroundResource(action.primaryStyle ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
|
||||
refreshButton.setTextColor(getColor(action.primaryStyle ? R.color.boss_surface : R.color.boss_green));
|
||||
}
|
||||
|
||||
private void syncTopActionVisualState(boolean refreshing) {
|
||||
if ("devices".equals(activeTab)) {
|
||||
configureTopAction("+添加", true);
|
||||
refreshButton.setEnabled(true);
|
||||
return;
|
||||
}
|
||||
configureTopAction(refreshing ? "同步中" : "刷新", false);
|
||||
refreshButton.setEnabled(!refreshing);
|
||||
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction(activeTab, refreshing);
|
||||
configureTopAction(action);
|
||||
refreshButton.setEnabled(!"refresh".equals(action.actionKey) || !refreshing);
|
||||
}
|
||||
|
||||
private void handleTopAction() {
|
||||
if ("devices".equals(activeTab)) {
|
||||
String actionKey = WechatSurfaceMapper.rootTopAction(activeTab, false).actionKey;
|
||||
if ("add_device".equals(actionKey)) {
|
||||
startActivity(new Intent(this, DeviceEnrollmentActivity.class));
|
||||
return;
|
||||
}
|
||||
if ("create_group_chat".equals(actionKey)) {
|
||||
startActivity(new Intent(this, GroupCreateActivity.class));
|
||||
return;
|
||||
}
|
||||
refreshCurrentTab();
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,16 @@ public final class WechatSurfaceMapper {
|
||||
return "cancel_on_detach";
|
||||
}
|
||||
|
||||
public static RootTopAction rootTopAction(String activeTab, boolean refreshing) {
|
||||
if ("devices".equals(activeTab)) {
|
||||
return new RootTopAction("+添加", true, "add_device");
|
||||
}
|
||||
if ("conversations".equals(activeTab)) {
|
||||
return new RootTopAction("+", true, "create_group_chat");
|
||||
}
|
||||
return new RootTopAction(refreshing ? "同步中" : "刷新", false, "refresh");
|
||||
}
|
||||
|
||||
public static <T> T resolveRefreshValue(T cachedValue, T freshValue, boolean requestSucceeded) {
|
||||
if (requestSucceeded) {
|
||||
return freshValue;
|
||||
@@ -170,6 +180,18 @@ public final class WechatSurfaceMapper {
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
public static final class RootTopAction {
|
||||
public final String label;
|
||||
public final boolean primaryStyle;
|
||||
public final String actionKey;
|
||||
|
||||
RootTopAction(String label, boolean primaryStyle, String actionKey) {
|
||||
this.label = label;
|
||||
this.primaryStyle = primaryStyle;
|
||||
this.actionKey = actionKey;
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildSubtitle(JSONObject source) {
|
||||
String status = localizeDeviceStatus(resolveDeviceStatusKey(source));
|
||||
String account = source.optString("account", "");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -13,7 +14,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="28dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
@@ -75,6 +76,7 @@
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -13,7 +14,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="28dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
@@ -75,6 +76,7 @@
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -13,7 +14,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="28dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
@@ -75,6 +76,7 @@
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -13,7 +14,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="28dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
@@ -75,6 +76,7 @@
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/login_shell"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
@@ -82,6 +83,7 @@
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/main_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -89,7 +91,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingRight="20dp"
|
||||
android:paddingRight="32dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<Button
|
||||
@@ -138,6 +140,7 @@
|
||||
android:id="@+id/refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="12dp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -13,7 +14,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="28dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<Button
|
||||
@@ -75,6 +76,7 @@
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
@@ -13,7 +14,7 @@
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="28dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
@@ -75,6 +76,7 @@
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class BossWindowInsetsTest {
|
||||
@Test
|
||||
public void applyStatusBarInset_addsInsetOnTopOfInitialPadding() {
|
||||
View view = new View(RuntimeEnvironment.getApplication());
|
||||
view.setPadding(12, 16, 18, 20);
|
||||
|
||||
BossWindowInsets.applyStatusBarInset(view);
|
||||
|
||||
WindowInsetsCompat insets = new WindowInsetsCompat.Builder()
|
||||
.setInsets(WindowInsetsCompat.Type.statusBars(), Insets.of(0, 30, 0, 0))
|
||||
.build();
|
||||
|
||||
WindowInsetsCompat applied = androidx.core.view.ViewCompat.dispatchApplyWindowInsets(view, insets);
|
||||
|
||||
assertEquals(12, view.getPaddingLeft());
|
||||
assertEquals(46, view.getPaddingTop());
|
||||
assertEquals(18, view.getPaddingRight());
|
||||
assertEquals(20, view.getPaddingBottom());
|
||||
assertEquals(insets, applied);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,30 @@ public class GroupCreateActivityTest {
|
||||
assertEquals("thread-1", filtered.get(0).optString("projectId", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectSelectableConversationItems_keepsAllThreadsWhenSourceConversationIsMissing() {
|
||||
JSONObject threadConversation = new StubJSONObject()
|
||||
.withString("projectId", "thread-1")
|
||||
.withString("projectTitle", "线程一")
|
||||
.withBoolean("isGroup", false);
|
||||
JSONObject secondThreadConversation = new StubJSONObject()
|
||||
.withString("projectId", "thread-2")
|
||||
.withString("projectTitle", "线程二")
|
||||
.withBoolean("isGroup", false);
|
||||
JSONObject groupConversation = new StubJSONObject()
|
||||
.withString("projectId", "group-1")
|
||||
.withString("projectTitle", "已有群聊")
|
||||
.withBoolean("isGroup", true);
|
||||
JSONObject conversationsPayload = new StubJSONObject()
|
||||
.withObjectArray("conversations", threadConversation, secondThreadConversation, groupConversation);
|
||||
|
||||
java.util.List<JSONObject> filtered = GroupCreateActivity.collectSelectableConversationItems(conversationsPayload, null);
|
||||
|
||||
assertEquals(2, filtered.size());
|
||||
assertEquals("thread-1", filtered.get(0).optString("projectId", ""));
|
||||
assertEquals("thread-2", filtered.get(1).optString("projectId", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconcileSelectedProjectIds_keepsManualDeselectionWhenCandidatesStayTheSame() {
|
||||
Set<String> previousCandidateIds = linkedSet("thread-1", "thread-2", "thread-3");
|
||||
@@ -57,10 +81,16 @@ public class GroupCreateActivityTest {
|
||||
public void canCreateGroupChat_blocksWhileRefreshingOrCreating() {
|
||||
Set<String> selectedProjectIds = linkedSet("thread-1");
|
||||
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(true, false, selectedProjectIds));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, true, selectedProjectIds));
|
||||
assertTrue(GroupCreateActivity.canCreateGroupChat(false, false, selectedProjectIds));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, false, linkedSet()));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(true, false, selectedProjectIds, true));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, true, selectedProjectIds, true));
|
||||
assertTrue(GroupCreateActivity.canCreateGroupChat(false, false, selectedProjectIds, true));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, false, linkedSet(), true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCreateGroupChat_requiresTwoSelectionsWhenCreatedFromConversationList() {
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, false, linkedSet("thread-1"), false));
|
||||
assertTrue(GroupCreateActivity.canCreateGroupChat(false, false, linkedSet("thread-1", "thread-2"), false));
|
||||
}
|
||||
|
||||
private static Set<String> linkedSet(String... values) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class WechatSurfaceMapperTopActionTest {
|
||||
@Test
|
||||
public void rootTopAction_usesPlusForConversations() {
|
||||
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction("conversations", false);
|
||||
|
||||
assertEquals("+", action.label);
|
||||
assertTrue(action.primaryStyle);
|
||||
assertEquals("create_group_chat", action.actionKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootTopAction_keepsAddDeviceOnDevicesTab() {
|
||||
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction("devices", false);
|
||||
|
||||
assertEquals("+添加", action.label);
|
||||
assertTrue(action.primaryStyle);
|
||||
assertEquals("add_device", action.actionKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootTopAction_keepsRefreshOnMeTab() {
|
||||
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction("me", true);
|
||||
|
||||
assertEquals("同步中", action.label);
|
||||
assertFalse(action.primaryStyle);
|
||||
assertEquals("refresh", action.actionKey);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user