SHA256
Добавить просмотр лайков сообщений канала
This commit is contained in:
+4
@@ -75,6 +75,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriend
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
|
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelMessages_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelMessages_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetMessageThread_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetMessageThread_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetMessageLikes_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetPersonalDiary_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetPersonalDiary_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetGroupDialog_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetGroupDialog_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelsCounters_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelsCounters_Handler;
|
||||||
@@ -85,6 +86,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsC
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageLikes_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalDiary_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalDiary_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
||||||
@@ -210,6 +212,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
|
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
|
||||||
Map.entry("GetPersonalDiary", new Net_GetPersonalDiary_Handler()),
|
Map.entry("GetPersonalDiary", new Net_GetPersonalDiary_Handler()),
|
||||||
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
|
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
|
||||||
|
Map.entry("GetMessageLikes", new Net_GetMessageLikes_Handler()),
|
||||||
Map.entry("GetGroupDialog", new Net_GetGroupDialog_Handler()),
|
Map.entry("GetGroupDialog", new Net_GetGroupDialog_Handler()),
|
||||||
Map.entry("ListGroupChats200", new Net_ListGroupChats200_Handler()),
|
Map.entry("ListGroupChats200", new Net_ListGroupChats200_Handler()),
|
||||||
Map.entry("GetChannelsCounters", new Net_GetChannelsCounters_Handler()),
|
Map.entry("GetChannelsCounters", new Net_GetChannelsCounters_Handler()),
|
||||||
@@ -302,6 +305,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetChannelMessages", Net_GetChannelMessages_Request.class),
|
Map.entry("GetChannelMessages", Net_GetChannelMessages_Request.class),
|
||||||
Map.entry("GetPersonalDiary", Net_GetPersonalDiary_Request.class),
|
Map.entry("GetPersonalDiary", Net_GetPersonalDiary_Request.class),
|
||||||
Map.entry("GetMessageThread", Net_GetMessageThread_Request.class),
|
Map.entry("GetMessageThread", Net_GetMessageThread_Request.class),
|
||||||
|
Map.entry("GetMessageLikes", Net_GetMessageLikes_Request.class),
|
||||||
Map.entry("GetGroupDialog", Net_GetGroupDialog_Request.class),
|
Map.entry("GetGroupDialog", Net_GetGroupDialog_Request.class),
|
||||||
Map.entry("ListGroupChats200", Net_ListGroupChats200_Request.class),
|
Map.entry("ListGroupChats200", Net_ListGroupChats200_Request.class),
|
||||||
Map.entry("GetChannelsCounters", Net_GetChannelsCounters_Request.class),
|
Map.entry("GetChannelsCounters", Net_GetChannelsCounters_Request.class),
|
||||||
|
|||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.channels;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageLikes_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageLikes_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_GetMessageLikes_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_GetMessageLikes_Handler.class);
|
||||||
|
private static final int HARD_LIMIT = 1000;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_GetMessageLikes_Request req = (Net_GetMessageLikes_Request) baseRequest;
|
||||||
|
Net_GetMessageLikes_Request.MessageSelector message = req.getMessage();
|
||||||
|
if (message == null || message.getBlockchainName() == null || message.getBlockchainName().isBlank()
|
||||||
|
|| message.getBlockNumber() == null || message.getBlockNumber() < 0
|
||||||
|
|| message.getBlockHash() == null || message.getBlockHash().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message");
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] blockHash;
|
||||||
|
try {
|
||||||
|
blockHash = ChannelsReadSupport.hexToBytes(message.getBlockHash());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_hash", "Некорректный blockHash");
|
||||||
|
}
|
||||||
|
if (blockHash == null || blockHash.length == 0) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_hash", "Некорректный blockHash");
|
||||||
|
}
|
||||||
|
|
||||||
|
int requested = req.getLimit() == null ? HARD_LIMIT : Math.max(1, Math.min(HARD_LIMIT, req.getLimit()));
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
SELECT rs.from_login,
|
||||||
|
COALESCE(ups.first_name, '') AS first_name,
|
||||||
|
COALESCE(ups.last_name, '') AS last_name,
|
||||||
|
COALESCE(ups.ava_ar, '') AS avatar_ar,
|
||||||
|
COALESCE(ups.account_role, 'unknown') AS account_role,
|
||||||
|
COALESCE(ups.shine_status, 'unknown') AS shine_status
|
||||||
|
FROM reactions_state rs
|
||||||
|
LEFT JOIN user_profile_state ups ON LOWER(ups.login) = LOWER(rs.from_login)
|
||||||
|
WHERE rs.reaction_type = 1
|
||||||
|
AND rs.last_sub_type = 1
|
||||||
|
AND rs.to_bch_name = ?
|
||||||
|
AND rs.to_block_number = ?
|
||||||
|
AND rs.to_block_hash = ?
|
||||||
|
ORDER BY LOWER(rs.from_login)
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
List<Net_GetMessageLikes_Response.UserItem> shining = new ArrayList<>();
|
||||||
|
List<Net_GetMessageLikes_Response.UserItem> official = new ArrayList<>();
|
||||||
|
List<Net_GetMessageLikes_Response.UserItem> others = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, message.getBlockchainName().trim());
|
||||||
|
ps.setInt(2, message.getBlockNumber());
|
||||||
|
ps.setBytes(3, blockHash);
|
||||||
|
ps.setInt(4, requested + 1);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
int accepted = 0;
|
||||||
|
boolean truncated = false;
|
||||||
|
while (rs.next()) {
|
||||||
|
if (accepted >= requested) {
|
||||||
|
truncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
accepted++;
|
||||||
|
Net_GetMessageLikes_Response.UserItem user = new Net_GetMessageLikes_Response.UserItem();
|
||||||
|
user.setLogin(rs.getString("from_login"));
|
||||||
|
user.setFirstName(rs.getString("first_name"));
|
||||||
|
user.setLastName(rs.getString("last_name"));
|
||||||
|
user.setAvatarAr(rs.getString("avatar_ar"));
|
||||||
|
|
||||||
|
boolean isOfficial = "primary".equalsIgnoreCase(rs.getString("account_role"));
|
||||||
|
boolean isShining = "shining".equalsIgnoreCase(rs.getString("shine_status"));
|
||||||
|
if (isOfficial && isShining) shining.add(user);
|
||||||
|
else if (isOfficial) official.add(user);
|
||||||
|
else others.add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_GetMessageLikes_Response resp = new Net_GetMessageLikes_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setShining(shining);
|
||||||
|
resp.setOfficial(official);
|
||||||
|
resp.setOthers(others);
|
||||||
|
resp.setTotal(shining.size() + official.size() + others.size());
|
||||||
|
resp.setTruncated(truncated);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("GetMessageLikes failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.channels.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetMessageLikes_Request extends Net_Request {
|
||||||
|
private MessageSelector message;
|
||||||
|
private Integer limit;
|
||||||
|
|
||||||
|
public MessageSelector getMessage() { return message; }
|
||||||
|
public void setMessage(MessageSelector message) { this.message = message; }
|
||||||
|
|
||||||
|
public Integer getLimit() { return limit; }
|
||||||
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
|
||||||
|
public static class MessageSelector {
|
||||||
|
private String blockchainName;
|
||||||
|
private Integer blockNumber;
|
||||||
|
private String blockHash;
|
||||||
|
|
||||||
|
public String getBlockchainName() { return blockchainName; }
|
||||||
|
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
||||||
|
|
||||||
|
public Integer getBlockNumber() { return blockNumber; }
|
||||||
|
public void setBlockNumber(Integer blockNumber) { this.blockNumber = blockNumber; }
|
||||||
|
|
||||||
|
public String getBlockHash() { return blockHash; }
|
||||||
|
public void setBlockHash(String blockHash) { this.blockHash = blockHash; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.channels.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_GetMessageLikes_Response extends Net_Response {
|
||||||
|
private List<UserItem> shining = new ArrayList<>();
|
||||||
|
private List<UserItem> official = new ArrayList<>();
|
||||||
|
private List<UserItem> others = new ArrayList<>();
|
||||||
|
private int total;
|
||||||
|
private boolean truncated;
|
||||||
|
|
||||||
|
public List<UserItem> getShining() { return shining; }
|
||||||
|
public void setShining(List<UserItem> shining) { this.shining = shining; }
|
||||||
|
public List<UserItem> getOfficial() { return official; }
|
||||||
|
public void setOfficial(List<UserItem> official) { this.official = official; }
|
||||||
|
public List<UserItem> getOthers() { return others; }
|
||||||
|
public void setOthers(List<UserItem> others) { this.others = others; }
|
||||||
|
public int getTotal() { return total; }
|
||||||
|
public void setTotal(int total) { this.total = total; }
|
||||||
|
public boolean isTruncated() { return truncated; }
|
||||||
|
public void setTruncated(boolean truncated) { this.truncated = truncated; }
|
||||||
|
|
||||||
|
public static class UserItem {
|
||||||
|
private String login;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private String avatarAr;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||||
|
public String getLastName() { return lastName; }
|
||||||
|
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||||
|
public String getAvatarAr() { return avatarAr; }
|
||||||
|
public void setAvatarAr(String avatarAr) { this.avatarAr = avatarAr; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.12.4
|
client.version=1.12.5
|
||||||
server.version=1.10.1
|
server.version=1.10.2
|
||||||
|
|||||||
@@ -14,13 +14,16 @@
|
|||||||
3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения:
|
3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения:
|
||||||
предки, фокус-сообщение, потомки.
|
предки, фокус-сообщение, потомки.
|
||||||
|
|
||||||
4. `GetPersonalDiary` — отдает виртуальную ленту `Личный дневник`, собранную из `STATUS_ACTION` текущего пользователя.
|
4. `GetMessageLikes` — отдает списки пользователей, поставивших лайк сообщению,
|
||||||
|
сгруппированные для UI по статусу профиля.
|
||||||
|
|
||||||
5. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
|
5. `GetPersonalDiary` — отдает виртуальную ленту `Личный дневник`, собранную из `STATUS_ACTION` текущего пользователя.
|
||||||
|
|
||||||
6. `ListGroupChats200` — отдает список групповых чатов типа `200`.
|
6. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
|
||||||
|
|
||||||
7. `GetGroupDialog` — отдает сообщения конкретного группового чата типа `200`.
|
7. `ListGroupChats200` — отдает список групповых чатов типа `200`.
|
||||||
|
|
||||||
|
8. `GetGroupDialog` — отдает сообщения конкретного группового чата типа `200`.
|
||||||
|
|
||||||
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
|
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
|
||||||
>
|
>
|
||||||
@@ -266,7 +269,62 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4) GetPersonalDiary
|
## 4) GetMessageLikes
|
||||||
|
|
||||||
|
Возвращает пользователей, которые поставили лайк конкретному сообщению канала.
|
||||||
|
|
||||||
|
- `message.blockchainName`, `message.blockNumber`, `message.blockHash` должны указывать на исходное сообщение.
|
||||||
|
- `limit` ограничивается сервером сверху значением `1000`.
|
||||||
|
- Пользователи группируются по состоянию профиля:
|
||||||
|
- `shining` — `account_role=primary` и `shine_status=shining`;
|
||||||
|
- `official` — `account_role=primary`, но без `shine_status=shining`;
|
||||||
|
- `others` — остальные пользователи.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetMessageLikes",
|
||||||
|
"requestId": "req-4",
|
||||||
|
"payload": {
|
||||||
|
"message": {
|
||||||
|
"blockchainName": "bob-001",
|
||||||
|
"blockNumber": 140,
|
||||||
|
"blockHash": "..."
|
||||||
|
},
|
||||||
|
"limit": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetMessageLikes",
|
||||||
|
"requestId": "req-4",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"shining": [
|
||||||
|
{ "login": "Alice", "firstName": "Alice", "lastName": "", "avatarAr": "ArweaveTxId..." }
|
||||||
|
],
|
||||||
|
"official": [],
|
||||||
|
"others": [
|
||||||
|
{ "login": "Carl", "firstName": "", "lastName": "", "avatarAr": "" }
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"truncated": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
- `bad_fields` — не передан `message` или обязательные поля ссылки на сообщение.
|
||||||
|
- `bad_hash` — `message.blockHash` не является корректным hex-хэшем блока.
|
||||||
|
- `internal_error` — внутренняя ошибка чтения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) GetPersonalDiary
|
||||||
|
|
||||||
Возвращает виртуальный канал `Личный дневник` для самого пользователя.
|
Возвращает виртуальный канал `Личный дневник` для самого пользователя.
|
||||||
|
|
||||||
@@ -289,7 +347,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4) GetChannelsCounters
|
## 6) GetChannelsCounters
|
||||||
|
|
||||||
### Request
|
### Request
|
||||||
```json
|
```json
|
||||||
@@ -321,7 +379,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5) ListGroupChats200
|
## 7) ListGroupChats200
|
||||||
|
|
||||||
### Request
|
### Request
|
||||||
```json
|
```json
|
||||||
@@ -361,7 +419,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6) GetGroupDialog
|
## 8) GetGroupDialog
|
||||||
|
|
||||||
### Request
|
### Request
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
- `ListSubscriptionsFeed` — экран списка каналов.
|
- `ListSubscriptionsFeed` — экран списка каналов.
|
||||||
- `GetChannelMessages` — сообщения конкретного канала.
|
- `GetChannelMessages` — сообщения конкретного канала.
|
||||||
- `GetMessageThread` — дерево обсуждения для сообщения.
|
- `GetMessageThread` — дерево обсуждения для сообщения.
|
||||||
|
- `GetMessageLikes` — списки пользователей, поставивших лайк сообщению.
|
||||||
|
|
||||||
2. **UI вкладки Каналы**:
|
2. **UI вкладки Каналы**:
|
||||||
- при открытии пытается загрузить реальный feed с сервера;
|
- при открытии пытается загрузить реальный feed с сервера;
|
||||||
@@ -33,12 +34,14 @@
|
|||||||
1. Вызвать `ListSubscriptionsFeed`.
|
1. Вызвать `ListSubscriptionsFeed`.
|
||||||
2. Для канала `ownedChannels[0]` вызвать `GetChannelMessages`.
|
2. Для канала `ownedChannels[0]` вызвать `GetChannelMessages`.
|
||||||
3. Для первого `messages[0]` вызвать `GetMessageThread`.
|
3. Для первого `messages[0]` вызвать `GetMessageThread`.
|
||||||
|
4. Для первого `messages[0]` вызвать `GetMessageLikes`.
|
||||||
|
|
||||||
### Ошибки
|
### Ошибки
|
||||||
1. `ListSubscriptionsFeed` с пустым login -> `bad_fields`.
|
1. `ListSubscriptionsFeed` с пустым login -> `bad_fields`.
|
||||||
2. `GetChannelMessages` с битым channel payload -> `bad_fields`.
|
2. `GetChannelMessages` с битым channel payload -> `bad_fields`.
|
||||||
3. `GetMessageThread` с несуществующим block -> `message_not_found`.
|
3. `GetMessageThread` с несуществующим block -> `message_not_found`.
|
||||||
4. `AddBlock(CreateChannel)` с уже существующим именем -> `channel_name_already_exists`.
|
4. `GetMessageLikes` с битым `message.blockHash` -> `bad_hash`.
|
||||||
|
5. `AddBlock(CreateChannel)` с уже существующим именем -> `channel_name_already_exists`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -91,6 +94,22 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 3.4 GetMessageLikes
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetMessageLikes",
|
||||||
|
"requestId": "debug-likes-1",
|
||||||
|
"payload": {
|
||||||
|
"message": {
|
||||||
|
"blockchainName": "TestUser1-001",
|
||||||
|
"blockNumber": 123,
|
||||||
|
"blockHash": "<hash-from-GetChannelMessages>"
|
||||||
|
},
|
||||||
|
"limit": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4) Что смотреть в ответах
|
## 4) Что смотреть в ответах
|
||||||
@@ -115,6 +134,11 @@
|
|||||||
- у узлов должны быть версии и счетчики.
|
- у узлов должны быть версии и счетчики.
|
||||||
- у каждого узла дополнительно может приходить `rawBlockB64` (Base64 сырого `block_bytes`).
|
- у каждого узла дополнительно может приходить `rawBlockB64` (Base64 сырого `block_bytes`).
|
||||||
|
|
||||||
|
### GetMessageLikes
|
||||||
|
- `payload.shining[]`, `payload.official[]`, `payload.others[]` — группы пользователей.
|
||||||
|
- у каждого пользователя есть `login`, `firstName`, `lastName`, `avatarAr`.
|
||||||
|
- `payload.truncated=true` означает, что сервер обрезал список по `limit`.
|
||||||
|
|
||||||
### Важно по совместимости
|
### Важно по совместимости
|
||||||
- `rawBlockB64` добавлен только в `GetMessageThread`.
|
- `rawBlockB64` добавлен только в `GetMessageThread`.
|
||||||
- `GetChannelMessages` не содержит `rawBlockB64` (без изменений формата ленты).
|
- `GetChannelMessages` не содержит `rawBlockB64` (без изменений формата ленты).
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
| `ListSubscriptionsFeed` | `06_Channels_Read_API.md` | лента каналов/подписок |
|
| `ListSubscriptionsFeed` | `06_Channels_Read_API.md` | лента каналов/подписок |
|
||||||
| `GetChannelMessages` | `06_Channels_Read_API.md` | сообщения канала |
|
| `GetChannelMessages` | `06_Channels_Read_API.md` | сообщения канала |
|
||||||
| `GetMessageThread` | `06_Channels_Read_API.md` | тред сообщения |
|
| `GetMessageThread` | `06_Channels_Read_API.md` | тред сообщения |
|
||||||
|
| `GetMessageLikes` | `06_Channels_Read_API.md` | списки пользователей, поставивших лайк сообщению |
|
||||||
| `GetPersonalDiary` | `06_Channels_Read_API.md` | виртуальный канал `Личный дневник` из STATUS_ACTION |
|
| `GetPersonalDiary` | `06_Channels_Read_API.md` | виртуальный канал `Личный дневник` из STATUS_ACTION |
|
||||||
| `GetChannelsCounters` | `06_Channels_Read_API.md` | счетчики разделов каналов |
|
| `GetChannelsCounters` | `06_Channels_Read_API.md` | счетчики разделов каналов |
|
||||||
| `ListGroupChats200` | `06_Channels_Read_API.md` | список групповых чатов типа `200` |
|
| `ListGroupChats200` | `06_Channels_Read_API.md` | список групповых чатов типа `200` |
|
||||||
|
|||||||
@@ -1790,6 +1790,169 @@ function renderChannelMetaEventCard(event) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function likeCategoryCounts(post) {
|
||||||
|
const total = Math.max(0, Number(post?.likesCount || 0));
|
||||||
|
const primary = Math.max(0, Math.min(total, Number(post?.primaryLikesCount || 0)));
|
||||||
|
const shining = Math.max(0, Math.min(primary, Number(post?.shiningLikesCount || 0)));
|
||||||
|
return {
|
||||||
|
shining,
|
||||||
|
official: Math.max(0, primary - shining),
|
||||||
|
others: Math.max(0, total - primary),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMessageLikesListModal({ navigate, messageRef, initialTab = 'shining' }) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'channel-likes-modal-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<section class="channel-likes-modal" role="dialog" aria-modal="true" aria-label="Кто поставил лайк">
|
||||||
|
<header class="channel-likes-modal__header">
|
||||||
|
<h3>Лайки</h3>
|
||||||
|
<button type="button" class="ui-button channel-likes-modal__close" aria-label="Закрыть">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="channel-likes-tabs" role="tablist">
|
||||||
|
<button type="button" class="ui-button" data-like-tab="shining">Сияющие <span data-like-tab-count="shining"></span></button>
|
||||||
|
<button type="button" class="ui-button" data-like-tab="official">Официальные <span data-like-tab-count="official"></span></button>
|
||||||
|
<button type="button" class="ui-button" data-like-tab="others">Остальные <span data-like-tab-count="others"></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="channel-likes-modal__status">Загрузка...</div>
|
||||||
|
<div class="channel-likes-user-list"></div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
document.body.append(overlay);
|
||||||
|
|
||||||
|
const modal = overlay.querySelector('.channel-likes-modal');
|
||||||
|
const status = overlay.querySelector('.channel-likes-modal__status');
|
||||||
|
const list = overlay.querySelector('.channel-likes-user-list');
|
||||||
|
const close = () => overlay.remove();
|
||||||
|
overlay.querySelector('.channel-likes-modal__close')?.addEventListener('click', close);
|
||||||
|
overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); });
|
||||||
|
modal?.addEventListener('click', (event) => event.stopPropagation());
|
||||||
|
|
||||||
|
let activeTab = ['shining', 'official', 'others'].includes(initialTab) ? initialTab : 'shining';
|
||||||
|
let payload = null;
|
||||||
|
|
||||||
|
const renderTab = () => {
|
||||||
|
overlay.querySelectorAll('[data-like-tab]').forEach((button) => {
|
||||||
|
const isActive = button.dataset.likeTab === activeTab;
|
||||||
|
button.classList.toggle('is-active', isActive);
|
||||||
|
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
if (!payload) return;
|
||||||
|
const rows = Array.isArray(payload?.[activeTab]) ? payload[activeTab] : [];
|
||||||
|
list.innerHTML = '';
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const userButton = document.createElement('button');
|
||||||
|
userButton.type = 'button';
|
||||||
|
userButton.className = 'ui-button card row profile-list-row channel-like-user-row';
|
||||||
|
const avatarRaw = String(row?.avatarAr || '').trim();
|
||||||
|
userButton.append(renderUserAvatar({
|
||||||
|
login: String(row?.login || '').trim() || 'unknown',
|
||||||
|
firstName: String(row?.firstName || ''),
|
||||||
|
lastName: String(row?.lastName || ''),
|
||||||
|
avatar: avatarRaw ? { ar: avatarRaw } : null,
|
||||||
|
size: 'md',
|
||||||
|
}));
|
||||||
|
const firstName = String(row?.firstName || '').trim();
|
||||||
|
const lastName = String(row?.lastName || '').trim();
|
||||||
|
const login = String(row?.login || '').trim();
|
||||||
|
const name = [firstName, lastName].filter(Boolean).join(' ') || login;
|
||||||
|
const text = document.createElement('div');
|
||||||
|
text.className = 'profile-list-row-text';
|
||||||
|
text.innerHTML = `<b>${escapeHtml(name)}</b><small>${escapeHtml(login)}</small>`;
|
||||||
|
userButton.append(text);
|
||||||
|
userButton.addEventListener('click', () => {
|
||||||
|
close();
|
||||||
|
navigate(makeProfileRoute(login));
|
||||||
|
});
|
||||||
|
list.append(userButton);
|
||||||
|
});
|
||||||
|
status.textContent = rows.length ? (payload?.truncated ? 'Показаны первые 1000 лайков.' : '') : 'В этом списке пока никого нет.';
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.querySelectorAll('[data-like-tab]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
activeTab = button.dataset.likeTab;
|
||||||
|
renderTab();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
renderTab();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
payload = await authService.getMessageLikes(messageRef, 1000);
|
||||||
|
if (!overlay.isConnected) return;
|
||||||
|
['shining', 'official', 'others'].forEach((key) => {
|
||||||
|
const countEl = overlay.querySelector(`[data-like-tab-count="${key}"]`);
|
||||||
|
if (countEl) countEl.textContent = String(Array.isArray(payload?.[key]) ? payload[key].length : 0);
|
||||||
|
});
|
||||||
|
renderTab();
|
||||||
|
} catch (error) {
|
||||||
|
if (!overlay.isConnected) return;
|
||||||
|
status.className = 'channel-likes-modal__status is-error';
|
||||||
|
status.textContent = toUserMessage(error, 'Не удалось загрузить список лайков.');
|
||||||
|
list.innerHTML = '';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMessageLikePopup({ anchor, post, navigate, onToggleLike }) {
|
||||||
|
document.querySelectorAll('.channel-like-popup-layer').forEach((el) => el.remove());
|
||||||
|
const counts = likeCategoryCounts(post);
|
||||||
|
const layer = document.createElement('div');
|
||||||
|
layer.className = 'channel-like-popup-layer';
|
||||||
|
layer.innerHTML = `
|
||||||
|
<section class="channel-like-popup" role="dialog" aria-label="Лайки сообщения">
|
||||||
|
<div class="channel-like-popup__title">Лайки</div>
|
||||||
|
<div class="channel-like-popup__counts">
|
||||||
|
<button type="button" class="ui-button channel-like-count" data-like-list="shining"><b>${counts.shining}</b><span>Сияющие</span></button>
|
||||||
|
<button type="button" class="ui-button channel-like-count" data-like-list="official"><b>${counts.official}</b><span>Официальные</span></button>
|
||||||
|
<button type="button" class="ui-button channel-like-count" data-like-list="others"><b>${counts.others}</b><span>Остальные</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="channel-like-popup__total">Всего лайков: <b>${counts.total}</b></div>
|
||||||
|
<button type="button" class="ui-button channel-like-popup__action">${post.reactionState === 'liked' ? 'Убрать свой лайк' : 'Добавить свой лайк'}</button>
|
||||||
|
<button type="button" class="ui-button channel-like-popup__close">Закрыть</button>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
document.body.append(layer);
|
||||||
|
const popup = layer.querySelector('.channel-like-popup');
|
||||||
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
const width = Math.min(330, Math.max(270, window.innerWidth - 24));
|
||||||
|
const left = Math.max(12, Math.min(window.innerWidth - width - 12, rect.left + rect.width / 2 - width / 2));
|
||||||
|
const estimatedHeight = 255;
|
||||||
|
const top = rect.bottom + estimatedHeight < window.innerHeight - 8
|
||||||
|
? rect.bottom + 8
|
||||||
|
: Math.max(8, rect.top - estimatedHeight - 8);
|
||||||
|
popup.style.width = `${width}px`;
|
||||||
|
popup.style.left = `${left}px`;
|
||||||
|
popup.style.top = `${top}px`;
|
||||||
|
|
||||||
|
const close = () => layer.remove();
|
||||||
|
layer.addEventListener('click', (event) => { if (event.target === layer) close(); });
|
||||||
|
popup.addEventListener('click', (event) => event.stopPropagation());
|
||||||
|
layer.querySelector('.channel-like-popup__close')?.addEventListener('click', close);
|
||||||
|
layer.querySelectorAll('[data-like-list]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const initialTab = button.dataset.likeList;
|
||||||
|
close();
|
||||||
|
openMessageLikesListModal({ navigate, messageRef: post.messageRef, initialTab });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
layer.querySelector('.channel-like-popup__action')?.addEventListener('click', async (event) => {
|
||||||
|
const button = event.currentTarget;
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
await onToggleLike(post.messageRef, post.reactionState === 'liked' ? 'unlike' : 'like');
|
||||||
|
close();
|
||||||
|
} catch (error) {
|
||||||
|
button.disabled = false;
|
||||||
|
showToast(toUserMessage(error, 'Не удалось изменить лайк.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderPostCard(post, {
|
function renderPostCard(post, {
|
||||||
navigate,
|
navigate,
|
||||||
selector,
|
selector,
|
||||||
@@ -1976,18 +2139,11 @@ function renderPostCard(post, {
|
|||||||
`;
|
`;
|
||||||
setActionTitle(likeButton, isPending ? 'Лайк...' : 'Лайк');
|
setActionTitle(likeButton, isPending ? 'Лайк...' : 'Лайк');
|
||||||
likeButton.disabled = isPending;
|
likeButton.disabled = isPending;
|
||||||
likeButton.addEventListener('click', async (event) => {
|
likeButton.addEventListener('click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
if (isPending) return;
|
if (isPending) return;
|
||||||
if (!isLiked) {
|
openMessageLikePopup({ anchor: event.currentTarget, post, navigate, onToggleLike });
|
||||||
const ok = window.confirm('Поставить лайк?');
|
|
||||||
if (!ok) return;
|
|
||||||
}
|
|
||||||
await longPressFeel(event.currentTarget, 130);
|
|
||||||
likeButton.disabled = true;
|
|
||||||
setActionTitle(likeButton, 'Лайк...');
|
|
||||||
await onToggleLike(post.messageRef, isLiked ? 'unlike' : 'like', { likeButton });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const replyButton = document.createElement('button');
|
const replyButton = document.createElement('button');
|
||||||
|
|||||||
@@ -1595,6 +1595,17 @@ export class AuthService {
|
|||||||
return response.payload || {};
|
return response.payload || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMessageLikes(message, limit = 1000) {
|
||||||
|
const normalizedMessage = {
|
||||||
|
blockchainName: String(message?.blockchainName || '').trim(),
|
||||||
|
blockNumber: Number(message?.blockNumber),
|
||||||
|
blockHash: String(message?.blockHash || '').trim(),
|
||||||
|
};
|
||||||
|
const response = await this.ws.request('GetMessageLikes', { message: normalizedMessage, limit });
|
||||||
|
if (response.status !== 200) throw opError('GetMessageLikes', response);
|
||||||
|
return response.payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
async getMessageThread(message, depthUp = 20, depthDown = 2, limitChildrenPerNode = 50, login = '') {
|
async getMessageThread(message, depthUp = 20, depthDown = 2, limitChildrenPerNode = 50, login = '') {
|
||||||
const normalizedMessage = {
|
const normalizedMessage = {
|
||||||
blockchainName: String(message?.blockchainName || '').trim(),
|
blockchainName: String(message?.blockchainName || '').trim(),
|
||||||
|
|||||||
@@ -683,3 +683,143 @@
|
|||||||
color: #ffb7c5;
|
color: #ffb7c5;
|
||||||
background: rgba(var(--shine-action-blue-rgb), 0.12);
|
background: rgba(var(--shine-action-blue-rgb), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Channel message likes: compact popup + full user list modal. */
|
||||||
|
.channel-like-popup-layer,
|
||||||
|
.channel-likes-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1600;
|
||||||
|
background: rgba(5, 9, 18, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-popup {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1601;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(20, 27, 44, 0.96);
|
||||||
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.34);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-popup__title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-popup__counts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-count {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-count b { font-size: 18px; }
|
||||||
|
.channel-like-count span { font-size: 10px; opacity: .72; overflow-wrap: anywhere; }
|
||||||
|
|
||||||
|
.channel-like-popup__total {
|
||||||
|
padding: 10px 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-popup__action,
|
||||||
|
.channel-like-popup__close {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
margin-top: 7px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-popup__close { opacity: .72; }
|
||||||
|
|
||||||
|
.channel-likes-modal-overlay {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px;
|
||||||
|
background: rgba(5, 9, 18, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-modal {
|
||||||
|
width: min(560px, 100%);
|
||||||
|
max-height: min(720px, calc(100vh - 28px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(18, 25, 42, 0.98);
|
||||||
|
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-modal__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-modal__header h3 { margin: 0; }
|
||||||
|
.channel-likes-modal__close { font-size: 24px; min-width: 38px; min-height: 38px; }
|
||||||
|
|
||||||
|
.channel-likes-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-tabs > button {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-tabs > button.is-active {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-tabs span { margin-left: 3px; opacity: .68; }
|
||||||
|
|
||||||
|
.channel-likes-modal__status {
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 0 16px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-likes-modal__status.is-error { opacity: 1; }
|
||||||
|
|
||||||
|
.channel-likes-user-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-like-user-row {
|
||||||
|
width: 100%;
|
||||||
|
margin: 4px 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user