SHA256
Добавили счётчик подписчиков в каналах и подписки пользователя
This commit is contained in:
@@ -30,6 +30,8 @@ public final class DatabaseInitializer {
|
|||||||
public static final int SCHEMA_VERSION_11 = 11;
|
public static final int SCHEMA_VERSION_11 = 11;
|
||||||
public static final int SCHEMA_VERSION_12 = 12;
|
public static final int SCHEMA_VERSION_12 = 12;
|
||||||
public static final int SCHEMA_VERSION_13 = 13;
|
public static final int SCHEMA_VERSION_13 = 13;
|
||||||
|
public static final int SCHEMA_VERSION_14 = 14;
|
||||||
|
public static final int SCHEMA_VERSION_15 = 15;
|
||||||
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
||||||
@@ -43,6 +45,8 @@ public final class DatabaseInitializer {
|
|||||||
public static final String POSTGRES_MIGRATION_V11_RESOURCE = "postgres/migration_v11.sql";
|
public static final String POSTGRES_MIGRATION_V11_RESOURCE = "postgres/migration_v11.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V12_RESOURCE = "postgres/migration_v12.sql";
|
public static final String POSTGRES_MIGRATION_V12_RESOURCE = "postgres/migration_v12.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V14_RESOURCE = "postgres/migration_v14.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V15_RESOURCE = "postgres/migration_v15.sql";
|
||||||
|
|
||||||
private DatabaseInitializer() {}
|
private DatabaseInitializer() {}
|
||||||
|
|
||||||
@@ -154,6 +158,14 @@ public final class DatabaseInitializer {
|
|||||||
runSqlScript(conn, POSTGRES_MIGRATION_V13_RESOURCE);
|
runSqlScript(conn, POSTGRES_MIGRATION_V13_RESOURCE);
|
||||||
currentVersion = SCHEMA_VERSION_13;
|
currentVersion = SCHEMA_VERSION_13;
|
||||||
}
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_14) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V14_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_14;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_15) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V15_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_15;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+96
@@ -99,6 +99,8 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
int deletedBlocks = deleteBlocksForChain(c, blockchainName);
|
int deletedBlocks = deleteBlocksForChain(c, blockchainName);
|
||||||
int deletedBlockchainState = deleteBlockchainStateForChain(c, blockchainName);
|
int deletedBlockchainState = deleteBlockchainStateForChain(c, blockchainName);
|
||||||
|
|
||||||
|
rebuildStatsState(c);
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
|
|
||||||
return new CleanupResult(
|
return new CleanupResult(
|
||||||
@@ -366,6 +368,100 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
""", blockchainName);
|
""", blockchainName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void rebuildStatsState(Connection c) throws SQLException {
|
||||||
|
try (PreparedStatement truncate = c.prepareStatement("""
|
||||||
|
TRUNCATE TABLE user_stats_state, channel_stats_state
|
||||||
|
""")) {
|
||||||
|
truncate.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.login,
|
||||||
|
COALESCE(own.owned_public_channels_count, 0),
|
||||||
|
COALESCE(fu.following_users_count, 0),
|
||||||
|
COALESCE(fc.following_channels_count, 0),
|
||||||
|
COALESCE(cf.close_friends_count, 0),
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT owner_login, COUNT(*)::INTEGER AS owned_public_channels_count
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE channel_type_code = 1
|
||||||
|
GROUP BY owner_login
|
||||||
|
) own ON LOWER(own.owner_login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS following_users_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 30
|
||||||
|
AND to_block_number = 0
|
||||||
|
GROUP BY login
|
||||||
|
) fu ON LOWER(fu.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS following_channels_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.channel_type_code = 1
|
||||||
|
GROUP BY cs.login
|
||||||
|
) fc ON LOWER(fc.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS close_friends_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 10
|
||||||
|
GROUP BY login
|
||||||
|
) cf ON LOWER(cf.login) = LOWER(u.login)
|
||||||
|
""")) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code,
|
||||||
|
COUNT(DISTINCT cs.login)::INTEGER AS subscribers_count,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM channel_names_state cn
|
||||||
|
LEFT JOIN connections_state cs
|
||||||
|
ON cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = cn.owner_bch_name
|
||||||
|
AND cs.to_block_number = cn.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = cn.channel_root_block_hash
|
||||||
|
WHERE cn.channel_type_code = 1
|
||||||
|
GROUP BY
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code
|
||||||
|
""")) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int executeDelete(Connection c, String sql, String value) throws SQLException {
|
private int executeDelete(Connection c, String sql, String value) throws SQLException {
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, value);
|
ps.setString(1, value);
|
||||||
|
|||||||
@@ -0,0 +1,620 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_stats_state (
|
||||||
|
login TEXT PRIMARY KEY REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||||
|
owned_public_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (owned_public_channels_count >= 0),
|
||||||
|
following_users_count INTEGER NOT NULL DEFAULT 0 CHECK (following_users_count >= 0),
|
||||||
|
following_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (following_channels_count >= 0),
|
||||||
|
close_friends_count INTEGER NOT NULL DEFAULT 0 CHECK (close_friends_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_stats_state_following_users_count
|
||||||
|
ON user_stats_state (following_users_count);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_stats_state (
|
||||||
|
owner_bch_name TEXT NOT NULL,
|
||||||
|
channel_root_block_number INTEGER NOT NULL CHECK (channel_root_block_number >= 0),
|
||||||
|
channel_root_block_hash BYTEA NOT NULL,
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
||||||
|
subscribers_count INTEGER NOT NULL DEFAULT 0 CHECK (subscribers_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_bch_name, channel_root_block_number, channel_root_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_stats_state_owner_login
|
||||||
|
ON channel_stats_state (owner_login);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_user_stats_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
public_subscribers_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
IF NEW.owner_login IS NULL OR btrim(NEW.owner_login) = '' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_login,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count = user_stats_state.owned_public_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
INTO public_subscribers_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.owner_bch_name = NEW.owner_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.channel_root_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.channel_root_block_hash
|
||||||
|
AND cn.channel_type_code = 1;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_bch_name,
|
||||||
|
NEW.channel_root_block_number,
|
||||||
|
NEW.channel_root_block_hash,
|
||||||
|
NEW.owner_login,
|
||||||
|
NEW.channel_type_code,
|
||||||
|
COALESCE(public_subscribers_count, 0),
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
WITH pending AS (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS cnt
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = NEW.owner_bch_name
|
||||||
|
AND cs.to_block_number = NEW.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = NEW.channel_root_block_hash
|
||||||
|
GROUP BY cs.login
|
||||||
|
)
|
||||||
|
UPDATE user_stats_state us
|
||||||
|
SET following_channels_count = GREATEST(0, us.following_channels_count - pending.cnt),
|
||||||
|
updated_at_ms = now_ms
|
||||||
|
FROM pending
|
||||||
|
WHERE us.login = pending.login;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_connection_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
resolved_login TEXT;
|
||||||
|
positive_rel_type INTEGER;
|
||||||
|
existed_before BOOLEAN;
|
||||||
|
target_channel_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type <> 3 THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type IN (10, 20, 30, 40, 50, 52, 54, 60, 70, 74) THEN
|
||||||
|
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
IF NEW.msg_sub_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = user_stats_state.close_friends_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.msg_sub_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = user_stats_state.following_users_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = channel_stats_state.subscribers_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
|
INSERT INTO connections_state (
|
||||||
|
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
NEW.msg_sub_type,
|
||||||
|
resolved_login,
|
||||||
|
NEW.to_bch_name,
|
||||||
|
COALESCE(NEW.to_block_number, 0),
|
||||||
|
COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
positive_rel_type := CASE NEW.msg_sub_type
|
||||||
|
WHEN 11 THEN 10
|
||||||
|
WHEN 21 THEN 20
|
||||||
|
WHEN 31 THEN 30
|
||||||
|
WHEN 41 THEN 40
|
||||||
|
WHEN 51 THEN 50
|
||||||
|
WHEN 53 THEN 52
|
||||||
|
WHEN 55 THEN 54
|
||||||
|
WHEN 61 THEN 60
|
||||||
|
WHEN 71 THEN 70
|
||||||
|
WHEN 75 THEN 74
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF positive_rel_type IS NULL OR resolved_login IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF positive_rel_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = GREATEST(0, user_stats_state.close_friends_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF positive_rel_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = GREATEST(0, user_stats_state.following_users_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = GREATEST(0, channel_stats_state.subscribers_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_user_stats_state_ai ON solana_user_pda_current;
|
||||||
|
CREATE TRIGGER trg_user_stats_state_ai
|
||||||
|
AFTER INSERT ON solana_user_pda_current
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_user_stats_state_ai();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_channel_names_state_stats_ai ON channel_names_state;
|
||||||
|
CREATE TRIGGER trg_channel_names_state_stats_ai
|
||||||
|
AFTER INSERT ON channel_names_state
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_channel_names_state_stats_ai();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_blocks_connection_state_ai ON blocks;
|
||||||
|
CREATE TRIGGER trg_blocks_connection_state_ai
|
||||||
|
AFTER INSERT ON blocks
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_blocks_connection_state_ai();
|
||||||
|
|
||||||
|
TRUNCATE TABLE user_stats_state, channel_stats_state;
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.login,
|
||||||
|
COALESCE(own.owned_public_channels_count, 0) AS owned_public_channels_count,
|
||||||
|
COALESCE(fu.following_users_count, 0) AS following_users_count,
|
||||||
|
COALESCE(fc.following_channels_count, 0) AS following_channels_count,
|
||||||
|
COALESCE(cf.close_friends_count, 0) AS close_friends_count,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) AS updated_at_ms
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT owner_login, COUNT(*)::INTEGER AS owned_public_channels_count
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE channel_type_code = 1
|
||||||
|
GROUP BY owner_login
|
||||||
|
) own ON LOWER(own.owner_login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS following_users_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 30
|
||||||
|
AND to_block_number = 0
|
||||||
|
GROUP BY login
|
||||||
|
) fu ON LOWER(fu.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS following_channels_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.channel_type_code = 1
|
||||||
|
GROUP BY cs.login
|
||||||
|
) fc ON LOWER(fc.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS close_friends_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 10
|
||||||
|
GROUP BY login
|
||||||
|
) cf ON LOWER(cf.login) = LOWER(u.login);
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code,
|
||||||
|
COUNT(DISTINCT cs.login)::INTEGER AS subscribers_count,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) AS updated_at_ms
|
||||||
|
FROM channel_names_state cn
|
||||||
|
LEFT JOIN connections_state cs
|
||||||
|
ON cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = cn.owner_bch_name
|
||||||
|
AND cs.to_block_number = cn.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = cn.channel_root_block_hash
|
||||||
|
WHERE cn.channel_type_code = 1
|
||||||
|
GROUP BY
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 14, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
public_subscribers_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
IF NEW.owner_login IS NULL OR btrim(NEW.owner_login) = '' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM solana_user_pda_current su
|
||||||
|
WHERE su.login = NEW.owner_login
|
||||||
|
) THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_login,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count = user_stats_state.owned_public_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
INTO public_subscribers_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.owner_bch_name = NEW.owner_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.channel_root_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.channel_root_block_hash
|
||||||
|
AND cn.channel_type_code = 1;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_bch_name,
|
||||||
|
NEW.channel_root_block_number,
|
||||||
|
NEW.channel_root_block_hash,
|
||||||
|
NEW.owner_login,
|
||||||
|
NEW.channel_type_code,
|
||||||
|
COALESCE(public_subscribers_count, 0),
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
WITH pending AS (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS cnt
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = NEW.owner_bch_name
|
||||||
|
AND cs.to_block_number = NEW.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = NEW.channel_root_block_hash
|
||||||
|
GROUP BY cs.login
|
||||||
|
)
|
||||||
|
UPDATE user_stats_state us
|
||||||
|
SET following_channels_count = GREATEST(0, us.following_channels_count - pending.cnt),
|
||||||
|
updated_at_ms = now_ms
|
||||||
|
FROM pending
|
||||||
|
WHERE us.login = pending.login;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 15, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -291,6 +291,12 @@ AFTER TRUNCATE ON solana_user_pda_current
|
|||||||
FOR EACH STATEMENT
|
FOR EACH STATEMENT
|
||||||
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate();
|
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_user_stats_state_ai ON solana_user_pda_current;
|
||||||
|
CREATE TRIGGER trg_user_stats_state_ai
|
||||||
|
AFTER INSERT ON solana_user_pda_current
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_user_stats_state_ai();
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS solana_user_pda_history (
|
CREATE TABLE IF NOT EXISTS solana_user_pda_history (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
tx_signature TEXT NOT NULL,
|
tx_signature TEXT NOT NULL,
|
||||||
@@ -605,6 +611,32 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
|
|||||||
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
||||||
ON channel_names_state (owner_login, owner_bch_name);
|
ON channel_names_state (owner_login, owner_bch_name);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_stats_state (
|
||||||
|
login TEXT PRIMARY KEY REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||||
|
owned_public_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (owned_public_channels_count >= 0),
|
||||||
|
following_users_count INTEGER NOT NULL DEFAULT 0 CHECK (following_users_count >= 0),
|
||||||
|
following_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (following_channels_count >= 0),
|
||||||
|
close_friends_count INTEGER NOT NULL DEFAULT 0 CHECK (close_friends_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_stats_state_following_users_count
|
||||||
|
ON user_stats_state (following_users_count);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_stats_state (
|
||||||
|
owner_bch_name TEXT NOT NULL,
|
||||||
|
channel_root_block_number INTEGER NOT NULL CHECK (channel_root_block_number >= 0),
|
||||||
|
channel_root_block_hash BYTEA NOT NULL,
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
||||||
|
subscribers_count INTEGER NOT NULL DEFAULT 0 CHECK (subscribers_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_bch_name, channel_root_block_number, channel_root_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_stats_state_owner_login
|
||||||
|
ON channel_stats_state (owner_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
CREATE TABLE IF NOT EXISTS chat200_state (
|
||||||
owner_login TEXT NOT NULL,
|
owner_login TEXT NOT NULL,
|
||||||
owner_bch_name TEXT NOT NULL,
|
owner_bch_name TEXT NOT NULL,
|
||||||
@@ -921,6 +953,132 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_user_stats_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
public_subscribers_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
IF NEW.owner_login IS NULL OR btrim(NEW.owner_login) = '' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM solana_user_pda_current su
|
||||||
|
WHERE su.login = NEW.owner_login
|
||||||
|
) THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_login,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count = user_stats_state.owned_public_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
INTO public_subscribers_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.owner_bch_name = NEW.owner_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.channel_root_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.channel_root_block_hash
|
||||||
|
AND cn.channel_type_code = 1;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_bch_name,
|
||||||
|
NEW.channel_root_block_number,
|
||||||
|
NEW.channel_root_block_hash,
|
||||||
|
NEW.owner_login,
|
||||||
|
NEW.channel_type_code,
|
||||||
|
COALESCE(public_subscribers_count, 0),
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
WITH pending AS (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS cnt
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = NEW.owner_bch_name
|
||||||
|
AND cs.to_block_number = NEW.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = NEW.channel_root_block_hash
|
||||||
|
GROUP BY cs.login
|
||||||
|
)
|
||||||
|
UPDATE user_stats_state us
|
||||||
|
SET following_channels_count = GREATEST(0, us.following_channels_count - pending.cnt),
|
||||||
|
updated_at_ms = now_ms
|
||||||
|
FROM pending
|
||||||
|
WHERE us.login = pending.login;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION shine_blocks_line_integrity_bi()
|
CREATE OR REPLACE FUNCTION shine_blocks_line_integrity_bi()
|
||||||
RETURNS TRIGGER
|
RETURNS TRIGGER
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
@@ -999,6 +1157,8 @@ AS $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
resolved_login TEXT;
|
resolved_login TEXT;
|
||||||
positive_rel_type INTEGER;
|
positive_rel_type INTEGER;
|
||||||
|
existed_before BOOLEAN;
|
||||||
|
target_channel_type INTEGER;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW.msg_type <> 3 THEN
|
IF NEW.msg_type <> 3 THEN
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
@@ -1011,10 +1171,159 @@ BEGIN
|
|||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
IF NEW.msg_sub_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = user_stats_state.close_friends_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.msg_sub_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = user_stats_state.following_users_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = channel_stats_state.subscribers_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
DELETE FROM connections_state
|
DELETE FROM connections_state
|
||||||
WHERE login = NEW.login
|
WHERE login = NEW.login
|
||||||
AND rel_type = NEW.msg_sub_type
|
AND rel_type = NEW.msg_sub_type
|
||||||
AND to_login = resolved_login;
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
INSERT INTO connections_state (
|
INSERT INTO connections_state (
|
||||||
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||||
@@ -1048,10 +1357,161 @@ BEGIN
|
|||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF positive_rel_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = GREATEST(0, user_stats_state.close_friends_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF positive_rel_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = GREATEST(0, user_stats_state.following_users_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = GREATEST(0, channel_stats_state.subscribers_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
DELETE FROM connections_state
|
DELETE FROM connections_state
|
||||||
WHERE login = NEW.login
|
WHERE login = NEW.login
|
||||||
AND rel_type = positive_rel_type
|
AND rel_type = positive_rel_type
|
||||||
AND to_login = resolved_login;
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
@@ -1231,4 +1691,10 @@ AFTER INSERT ON blocks
|
|||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION shine_blocks_edit_apply_ai();
|
EXECUTE FUNCTION shine_blocks_edit_apply_ai();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_channel_names_state_stats_ai ON channel_names_state;
|
||||||
|
CREATE TRIGGER trg_channel_names_state_stats_ai
|
||||||
|
AFTER INSERT ON channel_names_state
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_channel_names_state_stats_ai();
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
+27
@@ -16,6 +16,8 @@ import utils.blockchain.BlockchainNameUtil;
|
|||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
channel.setMetaUpdatedAtMs(meta.metaUpdatedAtMs);
|
channel.setMetaUpdatedAtMs(meta.metaUpdatedAtMs);
|
||||||
channel.setChannelTypeCode(meta.channelTypeCode);
|
channel.setChannelTypeCode(meta.channelTypeCode);
|
||||||
channel.setChannelTypeVersion(meta.channelTypeVersion);
|
channel.setChannelTypeVersion(meta.channelTypeVersion);
|
||||||
|
channel.setSubscribersCount(loadSubscribersCount(c, ownerBch, lineCode, meta.channelTypeCode));
|
||||||
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
||||||
rootRef.setBlockNumber(lineCode);
|
rootRef.setBlockNumber(lineCode);
|
||||||
rootRef.setBlockHash(req.getChannel().getChannelRootBlockHash());
|
rootRef.setBlockHash(req.getChannel().getChannelRootBlockHash());
|
||||||
@@ -180,4 +183,28 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int loadSubscribersCount(Connection c, String ownerBch, int rootNumber, int channelTypeCode) {
|
||||||
|
if (channelTypeCode != (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String sql = """
|
||||||
|
SELECT subscribers_count
|
||||||
|
FROM channel_stats_state
|
||||||
|
WHERE owner_bch_name = ?
|
||||||
|
AND channel_root_block_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerBch);
|
||||||
|
ps.setInt(2, rootNumber);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return 0;
|
||||||
|
return Math.max(0, rs.getInt("subscribers_count"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("GetChannelMessages: не удалось загрузить subscribers_count для {}#{}", ownerBch, rootNumber, e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -32,6 +32,7 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
private Long metaUpdatedAtMs;
|
private Long metaUpdatedAtMs;
|
||||||
private Integer channelTypeCode;
|
private Integer channelTypeCode;
|
||||||
private Integer channelTypeVersion;
|
private Integer channelTypeVersion;
|
||||||
|
private Integer subscribersCount;
|
||||||
private BlockRef channelRoot;
|
private BlockRef channelRoot;
|
||||||
|
|
||||||
public String getOwnerLogin() { return ownerLogin; }
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
@@ -67,6 +68,9 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public Integer getChannelTypeVersion() { return channelTypeVersion; }
|
public Integer getChannelTypeVersion() { return channelTypeVersion; }
|
||||||
public void setChannelTypeVersion(Integer channelTypeVersion) { this.channelTypeVersion = channelTypeVersion; }
|
public void setChannelTypeVersion(Integer channelTypeVersion) { this.channelTypeVersion = channelTypeVersion; }
|
||||||
|
|
||||||
|
public Integer getSubscribersCount() { return subscribersCount; }
|
||||||
|
public void setSubscribersCount(Integer subscribersCount) { this.subscribersCount = subscribersCount; }
|
||||||
|
|
||||||
public BlockRef getChannelRoot() { return channelRoot; }
|
public BlockRef getChannelRoot() { return channelRoot; }
|
||||||
public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; }
|
public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; }
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -11,11 +11,15 @@ import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Re
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
|
import shine.db.DbController;
|
||||||
import shine.db.dao.CurrentUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.CurrentUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class Net_GetUser_Handler implements JsonMessageHandler {
|
public class Net_GetUser_Handler implements JsonMessageHandler {
|
||||||
@@ -63,6 +67,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
resp.setSolanaKey(u.getSolanaKey());
|
resp.setSolanaKey(u.getSolanaKey());
|
||||||
resp.setBlockchainKey(u.getBlockchainKey());
|
resp.setBlockchainKey(u.getBlockchainKey());
|
||||||
resp.setClientKey(u.getClientKey());
|
resp.setClientKey(u.getClientKey());
|
||||||
|
loadUserStats(resp, u.getLogin());
|
||||||
|
|
||||||
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
||||||
// автоматически восстанавливаем её для существующего пользователя.
|
// автоматически восстанавливаем её для существующего пользователя.
|
||||||
@@ -125,4 +130,35 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
return new String(out);
|
return new String(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void loadUserStats(Net_GetUser_Response resp, String login) {
|
||||||
|
resp.setOwnedPublicChannelsCount(0);
|
||||||
|
resp.setFollowingUsersCount(0);
|
||||||
|
resp.setFollowingChannelsCount(0);
|
||||||
|
resp.setCloseFriendsCount(0);
|
||||||
|
String sql = """
|
||||||
|
SELECT owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count
|
||||||
|
FROM user_stats_state
|
||||||
|
WHERE login = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (Connection c = DbController.getInstance().getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.setOwnedPublicChannelsCount(rs.getInt("owned_public_channels_count"));
|
||||||
|
resp.setFollowingUsersCount(rs.getInt("following_users_count"));
|
||||||
|
resp.setFollowingChannelsCount(rs.getInt("following_channels_count"));
|
||||||
|
resp.setCloseFriendsCount(rs.getInt("close_friends_count"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("GetUser: не удалось загрузить статистику для login={}", login, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -43,6 +43,10 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
private String serverLastGlobalHash;
|
private String serverLastGlobalHash;
|
||||||
private Long serverBlockchainSizeBytes;
|
private Long serverBlockchainSizeBytes;
|
||||||
private Long serverBlockchainSizeLimitBytes;
|
private Long serverBlockchainSizeLimitBytes;
|
||||||
|
private Integer ownedPublicChannelsCount;
|
||||||
|
private Integer followingUsersCount;
|
||||||
|
private Integer followingChannelsCount;
|
||||||
|
private Integer closeFriendsCount;
|
||||||
|
|
||||||
public Boolean getExists() { return exists; }
|
public Boolean getExists() { return exists; }
|
||||||
public void setExists(Boolean exists) { this.exists = exists; }
|
public void setExists(Boolean exists) { this.exists = exists; }
|
||||||
@@ -74,4 +78,16 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
public Long getServerBlockchainSizeLimitBytes() { return serverBlockchainSizeLimitBytes; }
|
public Long getServerBlockchainSizeLimitBytes() { return serverBlockchainSizeLimitBytes; }
|
||||||
public void setServerBlockchainSizeLimitBytes(Long serverBlockchainSizeLimitBytes) { this.serverBlockchainSizeLimitBytes = serverBlockchainSizeLimitBytes; }
|
public void setServerBlockchainSizeLimitBytes(Long serverBlockchainSizeLimitBytes) { this.serverBlockchainSizeLimitBytes = serverBlockchainSizeLimitBytes; }
|
||||||
|
|
||||||
|
public Integer getOwnedPublicChannelsCount() { return ownedPublicChannelsCount; }
|
||||||
|
public void setOwnedPublicChannelsCount(Integer ownedPublicChannelsCount) { this.ownedPublicChannelsCount = ownedPublicChannelsCount; }
|
||||||
|
|
||||||
|
public Integer getFollowingUsersCount() { return followingUsersCount; }
|
||||||
|
public void setFollowingUsersCount(Integer followingUsersCount) { this.followingUsersCount = followingUsersCount; }
|
||||||
|
|
||||||
|
public Integer getFollowingChannelsCount() { return followingChannelsCount; }
|
||||||
|
public void setFollowingChannelsCount(Integer followingChannelsCount) { this.followingChannelsCount = followingChannelsCount; }
|
||||||
|
|
||||||
|
public Integer getCloseFriendsCount() { return closeFriendsCount; }
|
||||||
|
public void setCloseFriendsCount(Integer closeFriendsCount) { this.closeFriendsCount = closeFriendsCount; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import blockchain.body.ConnectionBody;
|
|||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
import blockchain.body.HeaderBody;
|
import blockchain.body.HeaderBody;
|
||||||
import blockchain.body.TextBody;
|
import blockchain.body.TextBody;
|
||||||
|
import shine.db.DbController;
|
||||||
import test.it.blockchain.AddBlockSender;
|
import test.it.blockchain.AddBlockSender;
|
||||||
import test.it.blockchain.ChainState;
|
import test.it.blockchain.ChainState;
|
||||||
import test.it.utils.TestConfig;
|
import test.it.utils.TestConfig;
|
||||||
@@ -13,6 +14,10 @@ import test.it.utils.log.TestResult;
|
|||||||
import test.it.utils.ws.WsSession;
|
import test.it.utils.ws.WsSession;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@@ -117,6 +122,27 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
st1.registerTextChannelRoot(newsRootBlock, newsRootHash);
|
st1.registerTextChannelRoot(newsRootBlock, newsRootHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CREATE_CHANNEL "Updates" — второй публичный канал того же владельца
|
||||||
|
int updatesRootBlock;
|
||||||
|
byte[] updatesRootHash;
|
||||||
|
{
|
||||||
|
var ln = st1.nextLineByType(ChainState.TYPE_TECH);
|
||||||
|
sender1.send(new CreateChannelBody(
|
||||||
|
0,
|
||||||
|
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||||
|
"Updates",
|
||||||
|
"",
|
||||||
|
CreateChannelBody.CHANNEL_TYPE_PUBLIC,
|
||||||
|
CreateChannelBody.CHANNEL_TYPE_VERSION_DEFAULT
|
||||||
|
), t);
|
||||||
|
|
||||||
|
updatesRootBlock = st1.lastBlockNumber();
|
||||||
|
updatesRootHash = st1.getHash32(updatesRootBlock);
|
||||||
|
assertNotNull(updatesRootHash);
|
||||||
|
|
||||||
|
st1.registerTextChannelRoot(updatesRootBlock, updatesRootHash);
|
||||||
|
}
|
||||||
|
|
||||||
// POST #0 в канал "News"
|
// POST #0 в канал "News"
|
||||||
int newsPost0Block;
|
int newsPost0Block;
|
||||||
byte[] newsPost0Hash;
|
byte[] newsPost0Hash;
|
||||||
@@ -188,7 +214,21 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
bch1, newsRootBlock, newsRootHash,
|
bch1, newsRootBlock, newsRootHash,
|
||||||
"U2 follows U1 channel 'News' (target=U1 CREATE_CHANNEL root)", t);
|
"U2 follows U1 channel 'News' (target=U1 CREATE_CHANNEL root)", t);
|
||||||
|
|
||||||
// 3) FRIEND взаимно (на HEADER)
|
// 3) U2 подписался на второй канал U1 "Updates"
|
||||||
|
sendConnection(sender2, st2, MsgSubType.CONNECTION_FOLLOW,
|
||||||
|
bch1, updatesRootBlock, updatesRootHash,
|
||||||
|
"U2 follows U1 channel 'Updates' (target=U1 CREATE_CHANNEL root)", t);
|
||||||
|
|
||||||
|
assertEquals(2, countConnectionsByOwner(u2, u1),
|
||||||
|
"U2 должен иметь две отдельные записи подписки на каналы U1");
|
||||||
|
assertEquals(2, countFollowingChannels(u2),
|
||||||
|
"following_channels_count должен учитывать два разных канала одного владельца");
|
||||||
|
assertEquals(1, countSubscribers(bch1, newsRootBlock, newsRootHash),
|
||||||
|
"У канала News должен быть 1 подписчик");
|
||||||
|
assertEquals(1, countSubscribers(bch1, updatesRootBlock, updatesRootHash),
|
||||||
|
"У канала Updates должен быть 1 подписчик");
|
||||||
|
|
||||||
|
// 4) FRIEND взаимно (на HEADER)
|
||||||
sendConnection(sender1, st1, MsgSubType.CONNECTION_CLOSE_FRIEND,
|
sendConnection(sender1, st1, MsgSubType.CONNECTION_CLOSE_FRIEND,
|
||||||
bch2, u2HeaderBlock, u2HeaderHash,
|
bch2, u2HeaderBlock, u2HeaderHash,
|
||||||
"U1 -> U2: FRIEND", t);
|
"U1 -> U2: FRIEND", t);
|
||||||
@@ -197,7 +237,7 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
bch1, u1HeaderBlock, u1HeaderHash,
|
bch1, u1HeaderBlock, u1HeaderHash,
|
||||||
"U2 -> U1: FRIEND", t);
|
"U2 -> U1: FRIEND", t);
|
||||||
|
|
||||||
// 4) CONTACT несколько
|
// 5) CONTACT несколько
|
||||||
sendConnection(sender1, st1, MsgSubType.CONNECTION_CONTACT,
|
sendConnection(sender1, st1, MsgSubType.CONNECTION_CONTACT,
|
||||||
bch2, u2HeaderBlock, u2HeaderHash,
|
bch2, u2HeaderBlock, u2HeaderHash,
|
||||||
"U1 -> U2: CONTACT", t);
|
"U1 -> U2: CONTACT", t);
|
||||||
@@ -236,7 +276,21 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
bch3, u3HeaderBlock, u3HeaderHash,
|
bch3, u3HeaderBlock, u3HeaderHash,
|
||||||
"U1 -> U3: CONTACT", t);
|
"U1 -> U3: CONTACT", t);
|
||||||
|
|
||||||
// 5) U1 убирает U2 из контактов (UNCONTACT)
|
// 6) U2 отписывается только от News
|
||||||
|
sendConnection(sender2, st2, MsgSubType.CONNECTION_UNFOLLOW,
|
||||||
|
bch1, newsRootBlock, newsRootHash,
|
||||||
|
"U2 unfollows U1 channel 'News'", t);
|
||||||
|
|
||||||
|
assertEquals(1, countConnectionsByOwner(u2, u1),
|
||||||
|
"После отписки от одного канала должна остаться одна запись подписки");
|
||||||
|
assertEquals(1, countFollowingChannels(u2),
|
||||||
|
"following_channels_count должен уменьшиться ровно на 1");
|
||||||
|
assertEquals(0, countSubscribers(bch1, newsRootBlock, newsRootHash),
|
||||||
|
"После отписки от News у канала должен остаться 0 подписчиков");
|
||||||
|
assertEquals(1, countSubscribers(bch1, updatesRootBlock, updatesRootHash),
|
||||||
|
"Подписка на Updates должна остаться");
|
||||||
|
|
||||||
|
// 7) U1 убирает U2 из контактов (UNCONTACT)
|
||||||
sendConnection(sender1, st1, MsgSubType.CONNECTION_UNCONTACT,
|
sendConnection(sender1, st1, MsgSubType.CONNECTION_UNCONTACT,
|
||||||
bch2, u2HeaderBlock, u2HeaderHash,
|
bch2, u2HeaderBlock, u2HeaderHash,
|
||||||
"U1 -> U2: UNCONTACT", t);
|
"U1 -> U2: UNCONTACT", t);
|
||||||
@@ -288,4 +342,58 @@ public class IT_03_AddBlock_NoAuth {
|
|||||||
toBlockHash32
|
toBlockHash32
|
||||||
), timeout);
|
), timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int countConnectionsByOwner(String login, String ownerLogin) {
|
||||||
|
return queryInt("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = ?
|
||||||
|
AND rel_type = 30
|
||||||
|
AND to_login = ?
|
||||||
|
""", login, ownerLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countFollowingChannels(String login) {
|
||||||
|
return queryInt("""
|
||||||
|
SELECT following_channels_count
|
||||||
|
FROM user_stats_state
|
||||||
|
WHERE login = ?
|
||||||
|
""", login);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countSubscribers(String ownerBlockchainName, int rootBlockNumber, byte[] rootBlockHash) {
|
||||||
|
return queryInt("""
|
||||||
|
SELECT COALESCE(subscribers_count, 0)
|
||||||
|
FROM channel_stats_state
|
||||||
|
WHERE owner_bch_name = ?
|
||||||
|
AND channel_root_block_number = ?
|
||||||
|
AND channel_root_block_hash = ?
|
||||||
|
""", ownerBlockchainName, rootBlockNumber, rootBlockHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int queryInt(String sql, Object... params) {
|
||||||
|
try (Connection c = DbController.getInstance().getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
for (int i = 0; i < params.length; i++) {
|
||||||
|
Object p = params[i];
|
||||||
|
if (p instanceof String s) {
|
||||||
|
ps.setString(i + 1, s);
|
||||||
|
} else if (p instanceof Integer n) {
|
||||||
|
ps.setInt(i + 1, n);
|
||||||
|
} else if (p instanceof byte[] bytes) {
|
||||||
|
ps.setBytes(i + 1, bytes);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported SQL param type: " + (p == null ? "null" : p.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
throw new IllegalStateException("Query returned no rows: " + sql);
|
||||||
|
}
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("DB query failed: " + sql, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,11 @@
|
|||||||
"serverLastGlobalNumber": 128,
|
"serverLastGlobalNumber": 128,
|
||||||
"serverLastGlobalHash": "4f...ab",
|
"serverLastGlobalHash": "4f...ab",
|
||||||
"serverBlockchainSizeBytes": 45212,
|
"serverBlockchainSizeBytes": 45212,
|
||||||
"serverBlockchainSizeLimitBytes": 100000
|
"serverBlockchainSizeLimitBytes": 100000,
|
||||||
|
"ownedPublicChannelsCount": 3,
|
||||||
|
"followingUsersCount": 18,
|
||||||
|
"followingChannelsCount": 27,
|
||||||
|
"closeFriendsCount": 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -68,6 +72,10 @@
|
|||||||
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
|
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
|
||||||
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
|
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
|
||||||
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
|
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
|
||||||
|
- `ownedPublicChannelsCount` — количество публичных каналов, владельцем которых является пользователь;
|
||||||
|
- `followingUsersCount` — количество пользователей, на которых подписан пользователь;
|
||||||
|
- `followingChannelsCount` — количество публичных каналов, на которые подписан пользователь;
|
||||||
|
- `closeFriendsCount` — количество близких друзей пользователя.
|
||||||
|
|
||||||
### Успешный ответ: пользователя нет
|
### Успешный ответ: пользователя нет
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@
|
|||||||
"avaSha256": "0123...",
|
"avaSha256": "0123...",
|
||||||
"avaSize": 248193,
|
"avaSize": 248193,
|
||||||
"metaUpdatedAtMs": 1760000000000,
|
"metaUpdatedAtMs": 1760000000000,
|
||||||
|
"subscribersCount": 128,
|
||||||
"channelRoot": { "blockNumber": 123, "blockHash": "..." }
|
"channelRoot": { "blockNumber": 123, "blockHash": "..." }
|
||||||
},
|
},
|
||||||
"metaEvents": [
|
"metaEvents": [
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# История изменений документации блокчейна
|
# История изменений документации блокчейна
|
||||||
|
|
||||||
|
## 2026-08-25 19:45:18 +0400
|
||||||
|
- Базовый коммит-ориентир: `3a58519`.
|
||||||
|
- Добавлены runtime-агрегаты статистики:
|
||||||
|
- `user_stats_state` хранит `owned_public_channels_count`, `following_users_count`, `following_channels_count`, `close_friends_count`;
|
||||||
|
- `channel_stats_state` хранит `subscribers_count` для публичных каналов.
|
||||||
|
- Обновление статистики выполняется атомарно вместе с записью блока в той же SQL-транзакции `AddBlock`.
|
||||||
|
- После полного resync статистика пересобирается из текущих `connections_state` и `channel_names_state`, чтобы старые данные продолжали работать.
|
||||||
|
|
||||||
|
## 2026-08-25 19:59:00 +0400
|
||||||
|
- Базовый коммит-ориентир: `working tree`.
|
||||||
|
- Триггер `channel_names_state` для статистики сделан устойчивым к старым данным, где канал уже есть, а владельца ещё нет в `solana_user_pda_current`.
|
||||||
|
- Добавлена совместимая миграция `v15`, чтобы существующие базы перестали падать на старых каналах при старте сервера.
|
||||||
|
|
||||||
## 2026-08-12 13:00:00 +0400
|
## 2026-08-12 13:00:00 +0400
|
||||||
- Базовый коммит-ориентир: `working tree`.
|
- Базовый коммит-ориентир: `working tree`.
|
||||||
- Канонический текстовый формат служебных тегов сокращён:
|
- Канонический текстовый формат служебных тегов сокращён:
|
||||||
|
|||||||
@@ -42,4 +42,6 @@
|
|||||||
## Обязательное сопровождение
|
## Обязательное сопровождение
|
||||||
- При любом изменении формата/правил блокчейна в коде документы этого каталога обновляются в том же наборе изменений.
|
- При любом изменении формата/правил блокчейна в коде документы этого каталога обновляются в том же наборе изменений.
|
||||||
- Обычный `AddBlock` сейчас пишет через `<blockchainName>.tmp_bch`, `<blockchainName>.write_check` и `<blockchainName>.write_pending`; эта схема и `BlockchainTmpRecoveryOnStartup` должны быть описаны в актуальной документации по синхронизации и recovery.
|
- Обычный `AddBlock` сейчас пишет через `<blockchainName>.tmp_bch`, `<blockchainName>.write_check` и `<blockchainName>.write_pending`; эта схема и `BlockchainTmpRecoveryOnStartup` должны быть описаны в актуальной документации по синхронизации и recovery.
|
||||||
|
- Для runtime-агрегатов статистики `user_stats_state` и `channel_stats_state` действует тот же принцип derived state: они обновляются вместе с `AddBlock` и полностью пересобираются при full resync.
|
||||||
|
- Если в старых данных есть канал владельца, которого ещё нет в `solana_user_pda_current`, сервер не падает: `channel_stats_state` всё равно обновляется, а `user_stats_state` создаётся только после появления пользователя в Solana PDA.
|
||||||
- Каждое обновление документов фиксируется в `CHANGELOG.md` с датой/временем и хэшем коммита-основания.
|
- Каждое обновление документов фиксируется в `CHANGELOG.md` с датой/временем и хэшем коммита-основания.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
@@ -93,6 +93,7 @@ import * as chatView from './pages/chat-view.js?v=202608221218';
|
|||||||
import * as userProfileView from './pages/user-profile-view.js';
|
import * as userProfileView from './pages/user-profile-view.js';
|
||||||
import * as channelsList from './pages/channels-list.js?v=202608221218';
|
import * as channelsList from './pages/channels-list.js?v=202608221218';
|
||||||
import * as channelView from './pages/channel-view.js';
|
import * as channelView from './pages/channel-view.js';
|
||||||
|
import * as channelAboutView from './pages/channel-about-view.js';
|
||||||
import * as channelThreadView from './pages/channel-thread-view.js';
|
import * as channelThreadView from './pages/channel-thread-view.js';
|
||||||
import * as addChannelView from './pages/add-channel-view.js';
|
import * as addChannelView from './pages/add-channel-view.js';
|
||||||
import * as addPersonalPublicChatView from './pages/add-personal-public-chat-view.js';
|
import * as addPersonalPublicChatView from './pages/add-personal-public-chat-view.js';
|
||||||
@@ -155,6 +156,7 @@ const routes = {
|
|||||||
user: userProfileView,
|
user: userProfileView,
|
||||||
'channels-list': channelsList,
|
'channels-list': channelsList,
|
||||||
'channel-view': channelView,
|
'channel-view': channelView,
|
||||||
|
'channel-about-view': channelAboutView,
|
||||||
'channel-thread-view': channelThreadView,
|
'channel-thread-view': channelThreadView,
|
||||||
'add-channel-view': addChannelView,
|
'add-channel-view': addChannelView,
|
||||||
'add-personal-public-chat-view': addPersonalPublicChatView,
|
'add-personal-public-chat-view': addPersonalPublicChatView,
|
||||||
@@ -212,6 +214,7 @@ const GUEST_ALLOWED_PAGES = new Set([
|
|||||||
'network-view',
|
'network-view',
|
||||||
'channels-list',
|
'channels-list',
|
||||||
'channel-view',
|
'channel-view',
|
||||||
|
'channel-about-view',
|
||||||
'channel-thread-view',
|
'channel-thread-view',
|
||||||
'user',
|
'user',
|
||||||
'contact-search-view',
|
'contact-search-view',
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const SHINE_CONNECTIONS_LOGO_SRC = '/assets/SHiNE_connections_blue.svg';
|
||||||
|
|
||||||
|
export function createShineConnectionsLogo({ className = '' } = {}) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = SHINE_CONNECTIONS_LOGO_SRC;
|
||||||
|
img.alt = '';
|
||||||
|
img.setAttribute('aria-hidden', 'true');
|
||||||
|
img.className = String(className || '').trim();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { resolveToolbarActive } from '../router.js';
|
import { resolveToolbarActive } from '../router.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { openAuthRequiredModal } from '../services/auth-required-modal.js';
|
import { openAuthRequiredModal } from '../services/auth-required-modal.js';
|
||||||
|
import { SHINE_CONNECTIONS_LOGO_SRC } from './shine-logo.js';
|
||||||
|
|
||||||
// iconImg — путь к неоновой PNG (если есть, рисуем картинку вместо эмодзи); glow — цвет доп.свечения
|
// iconImg — путь к неоновой PNG (если есть, рисуем картинку вместо эмодзи); glow — цвет доп.свечения
|
||||||
// активной/нажатой вкладки (var --tab-glow); hero — «герой»-вкладка (крупнее/ярче, всегда светится).
|
// активной/нажатой вкладки (var --tab-glow); hero — «герой»-вкладка (крупнее/ярче, всегда светится).
|
||||||
@@ -8,7 +9,7 @@ import { openAuthRequiredModal } from '../services/auth-required-modal.js';
|
|||||||
const ITEMS = [
|
const ITEMS = [
|
||||||
{ pageId: 'messages-list', label: 'Личные', icon: '💬', iconImg: '/assets/icon_lichnye.png', glow: 'rgba(0, 229, 255, .6)' },
|
{ pageId: 'messages-list', label: 'Личные', icon: '💬', iconImg: '/assets/icon_lichnye.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||||
{ pageId: 'channels-list', label: 'Каналы', icon: '📢', iconImg: '/assets/icon_kanaly.png', glow: 'rgba(0, 229, 255, .6)' },
|
{ pageId: 'channels-list', label: 'Каналы', icon: '📢', iconImg: '/assets/icon_kanaly.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||||
{ pageId: 'network-view', label: 'Связи', icon: '🕸', iconImg: '/assets/icon_svyazi.png', glow: 'rgba(0, 229, 255, .6)', hero: true },
|
{ pageId: 'network-view', label: 'Связи', icon: '🕸', iconImg: SHINE_CONNECTIONS_LOGO_SRC, glow: 'rgba(0, 229, 255, .6)', hero: true },
|
||||||
{ pageId: 'notifications-view', label: 'Уведомления', icon: '🔔', iconImg: '/assets/icon_uvedomleniya.png', glow: 'rgba(0, 229, 255, .6)' },
|
{ pageId: 'notifications-view', label: 'Уведомления', icon: '🔔', iconImg: '/assets/icon_uvedomleniya.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||||
{ pageId: 'profile-view', label: 'Профиль', icon: '👤', iconImg: '/assets/icon_profil.png', glow: 'rgba(0, 229, 255, .6)' },
|
{ pageId: 'profile-view', label: 'Профиль', icon: '👤', iconImg: '/assets/icon_profil.png', glow: 'rgba(0, 229, 255, .6)' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { renderHeader } from '../components/header.js';
|
||||||
|
import { authService, state } from '../state.js';
|
||||||
|
import { navigateBack } from '../router.js';
|
||||||
|
import { makeShineChannelRootRoute, makeShineChannelShortRoute } from '../services/shine-routes.js';
|
||||||
|
import { showToast } from '../services/channels-ux.js';
|
||||||
|
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||||
|
|
||||||
|
export const pageMeta = { id: 'channel-about-view', title: 'О канале' };
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHash(hash) {
|
||||||
|
const normalized = String(hash || '').trim().toLowerCase();
|
||||||
|
return normalized || '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSafeInt(value) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCachedChannel(ownerBlockchainName, channelRootBlockNumber, channelRootBlockHash) {
|
||||||
|
const ownerBch = String(ownerBlockchainName || '').trim().toLowerCase();
|
||||||
|
const rootNo = Number(channelRootBlockNumber);
|
||||||
|
const rootHash = normalizeHash(channelRootBlockHash);
|
||||||
|
const rows = Object.values(state.channelsIndex || {});
|
||||||
|
return rows.find((row) => (
|
||||||
|
String(row?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch
|
||||||
|
&& Number(row?.channel?.channelRoot?.blockNumber) === rootNo
|
||||||
|
&& normalizeHash(row?.channel?.channelRoot?.blockHash) === rootHash
|
||||||
|
)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChannelLink(route) {
|
||||||
|
if (!route) return '';
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.pathname = `/${String(route).replace(/^\/+/, '')}`;
|
||||||
|
url.hash = '';
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function statsText(value) {
|
||||||
|
return Number.isFinite(Number(value)) ? String(Math.max(0, Number(value))) : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render({ navigate, route, chrome }) {
|
||||||
|
const ownerBlockchainName = String(route?.params?.ownerBlockchainName || '').trim();
|
||||||
|
const channelRootBlockNumber = toSafeInt(route?.params?.channelRootBlockNumber);
|
||||||
|
const channelRootBlockHash = normalizeHash(route?.params?.channelRootBlockHash);
|
||||||
|
const channelRoute = makeShineChannelRootRoute({
|
||||||
|
ownerBlockchainName,
|
||||||
|
channelRootBlockNumber,
|
||||||
|
channelRootBlockHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
const screen = document.createElement('section');
|
||||||
|
screen.className = 'stack channels-screen channels-screen--channel-about';
|
||||||
|
|
||||||
|
const topbar = renderHeader({
|
||||||
|
title: 'О канале',
|
||||||
|
leftAction: {
|
||||||
|
label: '←',
|
||||||
|
onClick: () => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
navigateBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (channelRoute) navigate(channelRoute);
|
||||||
|
},
|
||||||
|
ariaLabel: 'Назад',
|
||||||
|
title: 'Назад',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
chrome?.setTopbar(topbar);
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card stack channel-about-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="stack" id="channel-about-content">
|
||||||
|
<div class="meta-muted">Загрузка данных канала…</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const footer = document.createElement('div');
|
||||||
|
footer.className = 'meta-muted screen-footer';
|
||||||
|
footer.textContent = 'О канале (channel-about-view)';
|
||||||
|
|
||||||
|
screen.append(card, footer);
|
||||||
|
|
||||||
|
const renderContent = (channel) => {
|
||||||
|
const cleanName = String(channel?.displayTitle || channel?.displayName || channel?.channelName || 'Канал').trim();
|
||||||
|
const ownerName = String(channel?.ownerLogin || channel?.ownerName || 'автор').trim();
|
||||||
|
const description = String(channel?.channelDescription || channel?.description || '').trim();
|
||||||
|
const subscribersCount = Number(channel?.subscribersCount || 0);
|
||||||
|
const aboutRoute = makeShineChannelRootRoute({
|
||||||
|
ownerBlockchainName: channel?.ownerBlockchainName || ownerBlockchainName,
|
||||||
|
channelRootBlockNumber: channel?.channelRoot?.blockNumber ?? channelRootBlockNumber,
|
||||||
|
channelRootBlockHash: channel?.channelRoot?.blockHash ?? channelRootBlockHash,
|
||||||
|
});
|
||||||
|
const channelLinkRoute = makeShineChannelShortRoute({
|
||||||
|
ownerBlockchainName: channel?.ownerBlockchainName || ownerBlockchainName,
|
||||||
|
channelName: channel?.channelName || '',
|
||||||
|
});
|
||||||
|
const channelLink = buildChannelLink(channelLinkRoute);
|
||||||
|
const changedAtMs = Number(channel?.metaUpdatedAtMs || 0);
|
||||||
|
const changedAtLabel = changedAtMs ? new Date(changedAtMs).toLocaleString('ru-RU') : '—';
|
||||||
|
const avatarState = String(channel?.avaAr || '').trim() ? 'Установлен' : 'Не установлен';
|
||||||
|
|
||||||
|
const content = card.querySelector('#channel-about-content');
|
||||||
|
if (!content) return;
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="channel-profile-modal-head">
|
||||||
|
<h2 class="modal-title">${escapeHtml(cleanName)}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="channel-meta-details-grid">
|
||||||
|
<span>Дата</span><strong>${escapeHtml(changedAtLabel)}</strong>
|
||||||
|
<span>Владелец</span><strong>${escapeHtml(ownerName)}</strong>
|
||||||
|
<span>Подписчиков</span><strong>${escapeHtml(statsText(subscribersCount))}</strong>
|
||||||
|
<span>Системное имя</span><code>${escapeHtml(String(channel?.channelName || '').trim() || 'channel')}</code>
|
||||||
|
<span>Название</span><strong>${escapeHtml(cleanName)}</strong>
|
||||||
|
<span>Описание</span><span style="white-space: pre-wrap;">${escapeHtml(description || 'Описание не задано.')}</span>
|
||||||
|
<span>Аватар</span><span>${escapeHtml(avatarState)}</span>
|
||||||
|
<span>Ссылка</span><span><a href="${escapeHtml(channelLink)}">${escapeHtml(channelLink)}</a></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions-grid">
|
||||||
|
<button class="secondary-btn" type="button" id="channel-about-open">Открыть канал</button>
|
||||||
|
<button class="secondary-btn" type="button" id="channel-about-copy">Скопировать ссылку</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
content.querySelector('#channel-about-open')?.addEventListener('click', () => {
|
||||||
|
if (!channelLinkRoute) return;
|
||||||
|
navigate(channelLinkRoute);
|
||||||
|
});
|
||||||
|
content.querySelector('#channel-about-copy')?.addEventListener('click', async () => {
|
||||||
|
if (!channelLink) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(channelLink);
|
||||||
|
showToast('Ссылка скопирована');
|
||||||
|
} catch (error) {
|
||||||
|
showToast(toUserMessage(error, 'Не удалось скопировать ссылку'), { kind: 'error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cached = findCachedChannel(ownerBlockchainName, channelRootBlockNumber, channelRootBlockHash);
|
||||||
|
if (cached?.channel) {
|
||||||
|
renderContent({
|
||||||
|
...cached.channel,
|
||||||
|
subscribersCount: cached.channel.subscribersCount ?? cached.subscribersCount ?? 0,
|
||||||
|
});
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const payload = await authService.getChannelMessages({
|
||||||
|
ownerBlockchainName,
|
||||||
|
channelRootBlockNumber,
|
||||||
|
channelRootBlockHash,
|
||||||
|
}, 1, 'asc', String(state.session.login || '').trim());
|
||||||
|
renderContent(payload?.channel || {});
|
||||||
|
} catch (error) {
|
||||||
|
const content = card.querySelector('#channel-about-content');
|
||||||
|
if (content) {
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="meta-muted">Не удалось загрузить данные канала.</div>
|
||||||
|
<div class="meta-muted">${escapeHtml(toUserMessage(error, 'Проверьте подключение к серверу и повторите попытку.'))}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
extractLoginFromBlockchainName,
|
extractLoginFromBlockchainName,
|
||||||
makeProfileRoute,
|
makeProfileRoute,
|
||||||
makeShineMessageRoute,
|
makeShineMessageRoute,
|
||||||
|
makeShineChannelAboutRoute,
|
||||||
} from '../services/shine-routes.js';
|
} from '../services/shine-routes.js';
|
||||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||||
|
|
||||||
@@ -2550,13 +2551,12 @@ export function render({ navigate, route, chrome }) {
|
|||||||
channelHeaderButton.disabled = false;
|
channelHeaderButton.disabled = false;
|
||||||
channelHeaderButton.onclick = (event) => {
|
channelHeaderButton.onclick = (event) => {
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
openAboutChannelModal(apiData.channel, {
|
const aboutRoute = makeShineChannelAboutRoute({
|
||||||
canEdit: apiData?.isOwnChannel === true && !apiData?.isDiary && !isStoriesChannel(apiData?.channel),
|
ownerBlockchainName: apiData?.channel?.ownerBlockchainName || apiData?.selector?.ownerBlockchainName || '',
|
||||||
onEdit: () => openEditChannelModal({
|
channelRootBlockNumber: apiData?.selector?.channelRootBlockNumber ?? apiData?.channel?.channelRoot?.blockNumber ?? '',
|
||||||
channel: apiData.channel,
|
channelRootBlockHash: apiData?.selector?.channelRootBlockHash ?? apiData?.channel?.channelRoot?.blockHash ?? '',
|
||||||
onSave: onEditChannelMeta,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
if (aboutRoute) navigate(aboutRoute);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (channelEntrypointButton) {
|
if (channelEntrypointButton) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
writeChannelNotificationsState,
|
writeChannelNotificationsState,
|
||||||
} from '../services/channels-ux.js';
|
} from '../services/channels-ux.js';
|
||||||
import { makeShineChannelRoute } from '../services/shine-routes.js';
|
import { makeShineChannelRoute } from '../services/shine-routes.js';
|
||||||
|
import { makeShineChannelShortRoute } from '../services/shine-routes.js';
|
||||||
import { renderAvatar } from '../components/avatar-image.js';
|
import { renderAvatar } from '../components/avatar-image.js';
|
||||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||||
@@ -64,10 +65,9 @@ function buildChannelRouteFromSummary(summary, fallbackId) {
|
|||||||
const ownerBch = String(summary?.channel?.ownerBlockchainName || '').trim();
|
const ownerBch = String(summary?.channel?.ownerBlockchainName || '').trim();
|
||||||
const ownerLogin = String(summary?.channel?.ownerLogin || '').trim();
|
const ownerLogin = String(summary?.channel?.ownerLogin || '').trim();
|
||||||
const channelName = String(summary?.channel?.channelName || '').trim();
|
const channelName = String(summary?.channel?.channelName || '').trim();
|
||||||
return makeShineChannelRoute({
|
return makeShineChannelShortRoute({
|
||||||
ownerLogin,
|
|
||||||
ownerBlockchainName: ownerBch,
|
ownerBlockchainName: ownerBch,
|
||||||
channelName: channelName || fallbackId,
|
channelName: channelName || fallbackId || ownerLogin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||||
|
import { createShineConnectionsLogo } from '../components/shine-logo.js';
|
||||||
import { directMessages } from '../mock-data.js';
|
import { directMessages } from '../mock-data.js';
|
||||||
import {
|
import {
|
||||||
addAppLogEntry,
|
addAppLogEntry,
|
||||||
@@ -30,11 +31,12 @@ import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speec
|
|||||||
import { showToast } from '../services/channels-ux.js';
|
import { showToast } from '../services/channels-ux.js';
|
||||||
import { buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js';
|
import { buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js';
|
||||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||||
|
import { makeProfileLinksRoute, makeProfileRoute } from '../services/shine-routes.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
||||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||||
|
|
||||||
function createChatHeaderParts(login) {
|
function createChatHeaderParts(login, navigate) {
|
||||||
const cleanLogin = String(login || '').trim() || 'unknown';
|
const cleanLogin = String(login || '').trim() || 'unknown';
|
||||||
|
|
||||||
const avatarSlot = document.createElement('span');
|
const avatarSlot = document.createElement('span');
|
||||||
@@ -47,6 +49,13 @@ function createChatHeaderParts(login) {
|
|||||||
});
|
});
|
||||||
avatarSlot.append(initialAvatar);
|
avatarSlot.append(initialAvatar);
|
||||||
|
|
||||||
|
const avatarButton = document.createElement('button');
|
||||||
|
avatarButton.type = 'button';
|
||||||
|
avatarButton.className = 'chat-header-avatar-btn';
|
||||||
|
avatarButton.title = `Профиль ${cleanLogin}`;
|
||||||
|
avatarButton.setAttribute('aria-label', `Открыть профиль ${cleanLogin}`);
|
||||||
|
avatarButton.append(avatarSlot);
|
||||||
|
|
||||||
const loginEl = document.createElement('span');
|
const loginEl = document.createElement('span');
|
||||||
loginEl.className = 'chat-header-login';
|
loginEl.className = 'chat-header-login';
|
||||||
loginEl.setAttribute('role', 'heading');
|
loginEl.setAttribute('role', 'heading');
|
||||||
@@ -75,7 +84,23 @@ function createChatHeaderParts(login) {
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
return { centerNode: loginEl, avatarSlot };
|
const connectionsButton = document.createElement('button');
|
||||||
|
connectionsButton.type = 'button';
|
||||||
|
connectionsButton.className = 'icon-btn chat-header-icon-btn chat-header-connections-btn';
|
||||||
|
connectionsButton.title = `Связи ${cleanLogin}`;
|
||||||
|
connectionsButton.setAttribute('aria-label', `Открыть связи ${cleanLogin}`);
|
||||||
|
connectionsButton.append(createShineConnectionsLogo({ className: 'chat-header-connections-logo' }));
|
||||||
|
connectionsButton.addEventListener('click', () => {
|
||||||
|
if (!cleanLogin || cleanLogin === 'unknown') return;
|
||||||
|
navigate(makeProfileLinksRoute(cleanLogin));
|
||||||
|
});
|
||||||
|
|
||||||
|
avatarButton.addEventListener('click', () => {
|
||||||
|
if (!cleanLogin || cleanLogin === 'unknown') return;
|
||||||
|
navigate(makeProfileRoute(cleanLogin));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { centerNode: loginEl, avatarButton, connectionsButton };
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncatePreviewText(value, maxLen = 72) {
|
function truncatePreviewText(value, maxLen = 72) {
|
||||||
@@ -293,7 +318,6 @@ function openChatActionsMenu({
|
|||||||
anchorY = 0,
|
anchorY = 0,
|
||||||
onCall,
|
onCall,
|
||||||
onVideoCall,
|
onVideoCall,
|
||||||
onInstantVideoCall,
|
|
||||||
onClearHistory,
|
onClearHistory,
|
||||||
onDeleteChat,
|
onDeleteChat,
|
||||||
}) {
|
}) {
|
||||||
@@ -305,8 +329,7 @@ function openChatActionsMenu({
|
|||||||
<div class="dm-floating-menu-layer" id="chat-header-actions-layer">
|
<div class="dm-floating-menu-layer" id="chat-header-actions-layer">
|
||||||
<div class="modal-card stack dm-message-actions-menu dm-message-actions-popover" id="${menuId}">
|
<div class="modal-card stack dm-message-actions-menu dm-message-actions-popover" id="${menuId}">
|
||||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-call">Звонок</button>
|
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-call">Звонок</button>
|
||||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-video-call">Звонок с поддержкой видео</button>
|
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-video-call">Видеозвонок</button>
|
||||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-instant-video-call">Видеозвонок</button>
|
|
||||||
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-clear-history">Очистить историю</button>
|
<button class="secondary-btn dm-message-action-btn" type="button" id="chat-menu-clear-history">Очистить историю</button>
|
||||||
<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="chat-menu-delete-chat">Удалить чат</button>
|
<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="chat-menu-delete-chat">Удалить чат</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -358,10 +381,6 @@ function openChatActionsMenu({
|
|||||||
close();
|
close();
|
||||||
if (typeof onVideoCall === 'function') await onVideoCall();
|
if (typeof onVideoCall === 'function') await onVideoCall();
|
||||||
});
|
});
|
||||||
root.querySelector('#chat-menu-instant-video-call')?.addEventListener('click', async () => {
|
|
||||||
close();
|
|
||||||
if (typeof onInstantVideoCall === 'function') await onInstantVideoCall();
|
|
||||||
});
|
|
||||||
root.querySelector('#chat-menu-clear-history')?.addEventListener('click', async () => {
|
root.querySelector('#chat-menu-clear-history')?.addEventListener('click', async () => {
|
||||||
close();
|
close();
|
||||||
if (typeof onClearHistory === 'function') await onClearHistory();
|
if (typeof onClearHistory === 'function') await onClearHistory();
|
||||||
@@ -993,7 +1012,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const log = document.createElement('div');
|
const log = document.createElement('div');
|
||||||
log.className = 'messages-log dm-messages-log';
|
log.className = 'messages-log dm-messages-log';
|
||||||
|
|
||||||
const chatHeaderParts = createChatHeaderParts(chatId);
|
const chatHeaderParts = createChatHeaderParts(chatId, navigate);
|
||||||
const chatHeader = renderHeader({
|
const chatHeader = renderHeader({
|
||||||
centerNode: chatHeaderParts.centerNode,
|
centerNode: chatHeaderParts.centerNode,
|
||||||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||||||
@@ -1016,7 +1035,6 @@ export function render({ navigate, route, chrome }) {
|
|||||||
anchorY: Number(event?.currentTarget?.getBoundingClientRect?.().bottom || event?.clientY || 0),
|
anchorY: Number(event?.currentTarget?.getBoundingClientRect?.().bottom || event?.clientY || 0),
|
||||||
onCall: () => handleStartCall('audio'),
|
onCall: () => handleStartCall('audio'),
|
||||||
onVideoCall: () => handleStartCall('video'),
|
onVideoCall: () => handleStartCall('video'),
|
||||||
onInstantVideoCall: () => handleStartCall('instant_video'),
|
|
||||||
onClearHistory: async () => {
|
onClearHistory: async () => {
|
||||||
openChatConfirmModal({
|
openChatConfirmModal({
|
||||||
title: 'Очистить историю?',
|
title: 'Очистить историю?',
|
||||||
@@ -1064,7 +1082,8 @@ export function render({ navigate, route, chrome }) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
chatHeader.querySelector('.header-left')?.append(chatHeaderParts.avatarSlot);
|
const chatHeaderLeft = chatHeader.querySelector('.header-left');
|
||||||
|
chatHeaderLeft?.append(chatHeaderParts.connectionsButton, chatHeaderParts.avatarButton);
|
||||||
chrome?.setTopbar(chatHeader);
|
chrome?.setTopbar(chatHeader);
|
||||||
|
|
||||||
if (!isKnownContact) {
|
if (!isKnownContact) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '../state.js';
|
} from '../state.js';
|
||||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||||
|
import { createShineConnectionsLogo } from '../components/shine-logo.js';
|
||||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||||
|
|
||||||
@@ -167,15 +168,11 @@ function compareChatRows(a, b) {
|
|||||||
export function render({ navigate, chrome }) {
|
export function render({ navigate, chrome }) {
|
||||||
const screen = document.createElement('section');
|
const screen = document.createElement('section');
|
||||||
screen.className = 'stack dm-screen dm-list-screen';
|
screen.className = 'stack dm-screen dm-list-screen';
|
||||||
const login = String(state.session.login || '').trim();
|
|
||||||
const head = document.createElement('header');
|
const head = document.createElement('header');
|
||||||
head.className = 'dm-head';
|
head.className = 'dm-head';
|
||||||
head.innerHTML = `
|
head.innerHTML = `
|
||||||
<div class="dm-head-brand">
|
<div class="dm-head-brand">
|
||||||
<div class="dm-head-hex">${(login[0] || 'A').toUpperCase()}</div>
|
<span class="dm-head-logo-wrap" aria-hidden="true"></span>
|
||||||
<div class="dm-head-id">
|
|
||||||
<span class="dm-head-name"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="dm-head-title">Чаты</h1>
|
<h1 class="dm-head-title">Чаты</h1>
|
||||||
<div class="dm-head-menu-wrap">
|
<div class="dm-head-menu-wrap">
|
||||||
@@ -191,6 +188,9 @@ export function render({ navigate, chrome }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
head.querySelector('.dm-head-logo-wrap')?.append(
|
||||||
|
createShineConnectionsLogo({ className: 'dm-head-logo' }),
|
||||||
|
);
|
||||||
const menuButton = head.querySelector('.dm-head-menu-btn');
|
const menuButton = head.querySelector('.dm-head-menu-btn');
|
||||||
menuButton?.append(createOverflowDots());
|
menuButton?.append(createOverflowDots());
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { profile } from '../mock-data.js';
|
import { profile } from '../mock-data.js';
|
||||||
import { state } from '../state.js';
|
import { authService, state } from '../state.js';
|
||||||
import {
|
import {
|
||||||
PROFILE_GENDER_FEMALE,
|
PROFILE_GENDER_FEMALE,
|
||||||
PROFILE_GENDER_MALE,
|
PROFILE_GENDER_MALE,
|
||||||
@@ -223,6 +223,12 @@ export function render({ navigate, chrome }) {
|
|||||||
let currentToggles = [];
|
let currentToggles = [];
|
||||||
let currentGender = 'unknown';
|
let currentGender = 'unknown';
|
||||||
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||||
|
let currentStats = {
|
||||||
|
ownedPublicChannelsCount: 0,
|
||||||
|
followingUsersCount: 0,
|
||||||
|
followingChannelsCount: 0,
|
||||||
|
closeFriendsCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
function syncIdentity() {
|
function syncIdentity() {
|
||||||
if (!identityEl) return;
|
if (!identityEl) return;
|
||||||
@@ -269,6 +275,21 @@ export function render({ navigate, chrome }) {
|
|||||||
updateToggleButton(shineBtn, 'Сияющий', shine.enabled);
|
updateToggleButton(shineBtn, 'Сияющий', shine.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStats() {
|
||||||
|
const stats = [
|
||||||
|
{ label: 'Собственные публичные каналы', value: currentStats.ownedPublicChannelsCount },
|
||||||
|
{ label: 'Подписки на пользователей', value: currentStats.followingUsersCount },
|
||||||
|
{ label: 'Подписки на каналы', value: currentStats.followingChannelsCount },
|
||||||
|
{ label: 'Близкие друзья', value: currentStats.closeFriendsCount },
|
||||||
|
];
|
||||||
|
stats.forEach((stat) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'card profile-param-item row';
|
||||||
|
row.innerHTML = `<div class="profile-param-value"><b>${escapeHtml(stat.label)}</b>: ${escapeHtml(String(Number(stat.value || 0)))}</div>`;
|
||||||
|
listWrap.append(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
officialBtn?.classList.add('profile-badge-trigger');
|
officialBtn?.classList.add('profile-badge-trigger');
|
||||||
shineBtn?.classList.add('profile-badge-trigger');
|
shineBtn?.classList.add('profile-badge-trigger');
|
||||||
officialBtn?.addEventListener('click', () => {
|
officialBtn?.addEventListener('click', () => {
|
||||||
@@ -286,6 +307,7 @@ export function render({ navigate, chrome }) {
|
|||||||
|
|
||||||
function renderFields(fields) {
|
function renderFields(fields) {
|
||||||
listWrap.innerHTML = '';
|
listWrap.innerHTML = '';
|
||||||
|
renderStats();
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'card profile-param-item row';
|
row.className = 'card profile-param-item row';
|
||||||
@@ -317,6 +339,12 @@ export function render({ navigate, chrome }) {
|
|||||||
];
|
];
|
||||||
currentGender = 'unknown';
|
currentGender = 'unknown';
|
||||||
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||||
|
currentStats = {
|
||||||
|
ownedPublicChannelsCount: 0,
|
||||||
|
followingUsersCount: 0,
|
||||||
|
followingChannelsCount: 0,
|
||||||
|
closeFriendsCount: 0,
|
||||||
|
};
|
||||||
syncIdentity();
|
syncIdentity();
|
||||||
updateAvatarUi();
|
updateAvatarUi();
|
||||||
updateTogglesUi();
|
updateTogglesUi();
|
||||||
@@ -325,11 +353,20 @@ export function render({ navigate, chrome }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const snapshot = await loadProfileSnapshot(login);
|
const [snapshot, user] = await Promise.all([
|
||||||
|
loadProfileSnapshot(login),
|
||||||
|
authService.getUser(login).catch(() => ({})),
|
||||||
|
]);
|
||||||
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
|
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
|
||||||
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
|
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
|
||||||
currentGender = snapshot.gender || 'unknown';
|
currentGender = snapshot.gender || 'unknown';
|
||||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||||
|
currentStats = {
|
||||||
|
ownedPublicChannelsCount: Number(user?.ownedPublicChannelsCount || 0),
|
||||||
|
followingUsersCount: Number(user?.followingUsersCount || 0),
|
||||||
|
followingChannelsCount: Number(user?.followingChannelsCount || 0),
|
||||||
|
closeFriendsCount: Number(user?.closeFriendsCount || 0),
|
||||||
|
};
|
||||||
syncIdentity();
|
syncIdentity();
|
||||||
updateAvatarUi();
|
updateAvatarUi();
|
||||||
updateTogglesUi();
|
updateTogglesUi();
|
||||||
|
|||||||
+45
-1
@@ -26,6 +26,7 @@ const PRETTY_PATHS = new Map([
|
|||||||
['add-channel-view', 'channels/new'],
|
['add-channel-view', 'channels/new'],
|
||||||
['add-personal-public-chat-view', 'channels/new-public-chat'],
|
['add-personal-public-chat-view', 'channels/new-public-chat'],
|
||||||
['channel-view', 'channel'],
|
['channel-view', 'channel'],
|
||||||
|
['channel-about-view', 'channel/about'],
|
||||||
['channel-thread-view', 'thread'],
|
['channel-thread-view', 'thread'],
|
||||||
['network-view', 'network'],
|
['network-view', 'network'],
|
||||||
['notifications-view', 'notifications'],
|
['notifications-view', 'notifications'],
|
||||||
@@ -52,6 +53,10 @@ const PRETTY_PATHS = new Map([
|
|||||||
['remote-addblock-session-view', 'remote-addblock-session'],
|
['remote-addblock-session-view', 'remote-addblock-session'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function looksLikeBlockchainName(value) {
|
||||||
|
return /^.+-\d+$/.test(String(value || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
export const PRE_AUTH_PAGES = [
|
export const PRE_AUTH_PAGES = [
|
||||||
'start-view',
|
'start-view',
|
||||||
'entry-settings-view',
|
'entry-settings-view',
|
||||||
@@ -162,6 +167,34 @@ export function parseRouteFromPath(pathname = '') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (segments.length >= 2 && looksLikeBlockchainName(segments[0])) {
|
||||||
|
const ownerBlockchainName = decodePart(segments[0]);
|
||||||
|
const channelName = decodePart(segments[1] || '');
|
||||||
|
const sub = decodePart(segments[2] || '').toLowerCase();
|
||||||
|
if (ownerBlockchainName && channelName) {
|
||||||
|
if (sub === 'about') {
|
||||||
|
return {
|
||||||
|
pageId: 'channel-about-view',
|
||||||
|
params: {
|
||||||
|
ownerBlockchainName,
|
||||||
|
channelRootBlockNumber: '',
|
||||||
|
channelRootBlockHash: '',
|
||||||
|
channelId: '',
|
||||||
|
channelName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pageId: 'channel-view',
|
||||||
|
params: {
|
||||||
|
ownerBlockchainName,
|
||||||
|
channelName,
|
||||||
|
channelId: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (pageId === 'chat' || pageId === 'chat-view') {
|
if (pageId === 'chat' || pageId === 'chat-view') {
|
||||||
return { pageId: 'chat-view', params: { chatId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
|
return { pageId: 'chat-view', params: { chatId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
|
||||||
}
|
}
|
||||||
@@ -225,6 +258,17 @@ export function parseRouteFromPath(pathname = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pageId === 'channel') {
|
if (pageId === 'channel') {
|
||||||
|
if (segments.length >= 5 && decodePart(segments[4] || '').toLowerCase() === 'about') {
|
||||||
|
return {
|
||||||
|
pageId: 'channel-about-view',
|
||||||
|
params: {
|
||||||
|
ownerBlockchainName: decodePart(segments[1]),
|
||||||
|
channelRootBlockNumber: segments[2] || '',
|
||||||
|
channelRootBlockHash: segments[3] || '',
|
||||||
|
channelId: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
if (segments.length >= 4) {
|
if (segments.length >= 4) {
|
||||||
return {
|
return {
|
||||||
pageId: 'channel-view',
|
pageId: 'channel-view',
|
||||||
@@ -408,7 +452,7 @@ export function resolveToolbarActive(pageId) {
|
|||||||
pageId === 'solana-users-init-view'
|
pageId === 'solana-users-init-view'
|
||||||
) return 'profile-view';
|
) return 'profile-view';
|
||||||
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
|
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
|
||||||
if (pageId === 'channel-view' || pageId === 'channel-thread-view' || pageId === 'add-channel-view' || pageId === 'add-personal-public-chat-view') return 'channels-list';
|
if (pageId === 'channel-view' || pageId === 'channel-about-view' || pageId === 'channel-thread-view' || pageId === 'add-channel-view' || pageId === 'add-personal-public-chat-view') return 'channels-list';
|
||||||
if (pageId === 'user') return 'messages-list';
|
if (pageId === 'user') return 'messages-list';
|
||||||
return 'profile-view';
|
return 'profile-view';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function getCallTitleText(mode) {
|
|||||||
return 'Видеозвонок';
|
return 'Видеозвонок';
|
||||||
}
|
}
|
||||||
if (normalizeCallMode(mode) === CALL_MODE_VIDEO) {
|
if (normalizeCallMode(mode) === CALL_MODE_VIDEO) {
|
||||||
return 'Звонок с поддержкой видео';
|
return 'Видеозвонок';
|
||||||
}
|
}
|
||||||
return 'Звонок';
|
return 'Звонок';
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ function getIncomingCallStatusText(peerLogin, mode) {
|
|||||||
return `Входящий видеозвонок от ${name}`;
|
return `Входящий видеозвонок от ${name}`;
|
||||||
}
|
}
|
||||||
if (isVideoCallMode(mode)) {
|
if (isVideoCallMode(mode)) {
|
||||||
return `Вам звонит ${name} (звонок с поддержкой видео)`;
|
return `Входящий видеозвонок от ${name}`;
|
||||||
}
|
}
|
||||||
return `Вам звонит ${name}`;
|
return `Вам звонит ${name}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,26 @@ export function makeShineChannelRoute({ ownerLogin = '', ownerBlockchainName = '
|
|||||||
return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`;
|
return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeShineChannelRootRoute({ ownerBlockchainName = '', channelRootBlockNumber = '', channelRootBlockHash = '' }) {
|
||||||
|
const ownerBch = String(ownerBlockchainName || '').trim();
|
||||||
|
const rootNo = String(channelRootBlockNumber || '').trim();
|
||||||
|
const rootHash = String(channelRootBlockHash || '').trim();
|
||||||
|
if (!ownerBch || !rootNo || !rootHash) return '';
|
||||||
|
return `channel/${encodeRoutePart(ownerBch)}/${encodeRoutePart(rootNo)}/${encodeRoutePart(rootHash)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeShineChannelAboutRoute({ ownerBlockchainName = '', channelRootBlockNumber = '', channelRootBlockHash = '' }) {
|
||||||
|
const base = makeShineChannelRootRoute({ ownerBlockchainName, channelRootBlockNumber, channelRootBlockHash });
|
||||||
|
return base ? `${base}/about` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeShineChannelShortRoute({ ownerBlockchainName = '', channelName = '' }) {
|
||||||
|
const ownerBch = String(ownerBlockchainName || '').trim();
|
||||||
|
const chName = String(channelName || '').trim();
|
||||||
|
if (!ownerBch || !chName) return '';
|
||||||
|
return `${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function makeShineMessageRoute({ ownerLogin = '', messageBlockchainName = '', messageBlockNumber = '' }) {
|
export function makeShineMessageRoute({ ownerLogin = '', messageBlockchainName = '', messageBlockNumber = '' }) {
|
||||||
const msgBch = String(messageBlockchainName || '').trim();
|
const msgBch = String(messageBlockchainName || '').trim();
|
||||||
const msgNo = String(messageBlockNumber || '').trim();
|
const msgNo = String(messageBlockNumber || '').trim();
|
||||||
|
|||||||
@@ -5927,6 +5927,14 @@ textarea.input {
|
|||||||
transform: translateY(-40%);
|
transform: translateY(-40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channels-screen--channel-about .screen-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 8px 4px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
.entrypoint-history-list {
|
.entrypoint-history-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -6184,12 +6192,13 @@ textarea.input {
|
|||||||
background: linear-gradient(180deg, rgba(10,12,18,0.82), rgba(10,12,18,0.0));
|
background: linear-gradient(180deg, rgba(10,12,18,0.82), rgba(10,12,18,0.0));
|
||||||
}
|
}
|
||||||
.dm-head-brand { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
.dm-head-brand { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||||
.dm-head-hex {
|
.dm-head-logo-wrap { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; flex: 0 0 auto; }
|
||||||
width: 32px; height: 32px; flex: 0 0 auto; display: grid; place-items: center;
|
.dm-head-logo {
|
||||||
font-weight: 700; font-size: 15px; color: #1a1205;
|
width: 36px;
|
||||||
background: linear-gradient(150deg, #F0B82E, #D49F22);
|
height: 36px;
|
||||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
object-fit: contain;
|
||||||
box-shadow: 0 0 14px rgba(240, 184, 46, 0.35);
|
display: block;
|
||||||
|
filter: drop-shadow(0 0 7px rgba(71, 196, 255, 0.38));
|
||||||
}
|
}
|
||||||
.dm-head-id { min-width: 0; display: grid; }
|
.dm-head-id { min-width: 0; display: grid; }
|
||||||
.dm-head-name { font-size: 15px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.dm-head-name { font-size: 15px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
@@ -9100,6 +9109,66 @@ body.chat-topbar-overlay .app-shell.keyboard-open .scroll-to-bottom-btn {
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-header-connections-btn,
|
||||||
|
.chat-header-avatar-btn {
|
||||||
|
padding: 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-connections-btn:hover,
|
||||||
|
.chat-header-connections-btn:active,
|
||||||
|
.chat-header-connections-btn:focus,
|
||||||
|
.chat-header-connections-btn:focus-visible,
|
||||||
|
.chat-header-avatar-btn:hover,
|
||||||
|
.chat-header-avatar-btn:active,
|
||||||
|
.chat-header-avatar-btn:focus,
|
||||||
|
.chat-header-avatar-btn:focus-visible {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-connections-btn {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(8, 17, 31, 0.65);
|
||||||
|
border: 1px solid rgba(97, 198, 255, 0.26);
|
||||||
|
box-shadow: 0 0 0 1px rgba(97, 198, 255, 0.08), 0 0 12px rgba(64, 172, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-connections-btn:hover,
|
||||||
|
.chat-header-connections-btn:focus-visible {
|
||||||
|
border-color: rgba(111, 208, 255, 0.38);
|
||||||
|
box-shadow: 0 0 0 1px rgba(111, 208, 255, 0.16), 0 0 16px rgba(64, 172, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-connections-logo {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-avatar-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-avatar-btn .chat-header-avatar-slot {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
/* Пузырям сообщений не нужна отдельная обводка: направление читается по фону и геометрии. */
|
/* Пузырям сообщений не нужна отдельная обводка: направление читается по фону и геометрии. */
|
||||||
.dm-chat-screen .bubble,
|
.dm-chat-screen .bubble,
|
||||||
.dm-chat-screen .bubble.in,
|
.dm-chat-screen .bubble.in,
|
||||||
|
|||||||
Reference in New Issue
Block a user