fix: restore safe top actions and home group chat entry

This commit is contained in:
kris
2026-03-29 18:44:53 +08:00
parent e9ab62e94d
commit c6e8d19ee5
22 changed files with 467 additions and 127 deletions

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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
}
});
}
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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", "");

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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);
}
}