mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: implement channel membership management and ensure default channels are public
This commit is contained in:
@@ -712,12 +712,10 @@ public sealed class AppOrchestrator : IDisposable
|
|||||||
_joinedChannels.Add(channel.Name);
|
_joinedChannels.Add(channel.Name);
|
||||||
var history = await _connection!.JoinChannelAsync(channel.Name);
|
var history = await _connection!.JoinChannelAsync(channel.Name);
|
||||||
|
|
||||||
// Refresh the channel list and ensure private channels show up
|
|
||||||
var channels = await _apiClient.GetChannelsAsync();
|
|
||||||
InvokeUI(() =>
|
InvokeUI(() =>
|
||||||
{
|
{
|
||||||
_mainWindow.SetChannels(channels);
|
|
||||||
_mainWindow.EnsureChannelInList(channel.Name);
|
_mainWindow.EnsureChannelInList(channel.Name);
|
||||||
|
_mainWindow.SetChannelTopic(channel.Name, channel.Topic);
|
||||||
_mainWindow.SwitchToChannel(channel.Name);
|
_mainWindow.SwitchToChannel(channel.Name);
|
||||||
if (history.Count > 0)
|
if (history.Count > 0)
|
||||||
_mainWindow.LoadHistory(channel.Name, history);
|
_mainWindow.LoadHistory(channel.Name, history);
|
||||||
@@ -756,10 +754,9 @@ public sealed class AppOrchestrator : IDisposable
|
|||||||
await _apiClient!.DeleteChannelAsync(channel);
|
await _apiClient!.DeleteChannelAsync(channel);
|
||||||
_joinedChannels.Remove(channel);
|
_joinedChannels.Remove(channel);
|
||||||
|
|
||||||
var channels = await _apiClient.GetChannelsAsync();
|
|
||||||
InvokeUI(() =>
|
InvokeUI(() =>
|
||||||
{
|
{
|
||||||
_mainWindow.SetChannels(channels);
|
_mainWindow.RemoveChannel(channel);
|
||||||
_mainWindow.SwitchToChannel(HubConstants.DefaultChannel);
|
_mainWindow.SwitchToChannel(HubConstants.DefaultChannel);
|
||||||
_mainWindow.AddSystemMessage(HubConstants.DefaultChannel, $"Channel #{channel} has been deleted.");
|
_mainWindow.AddSystemMessage(HubConstants.DefaultChannel, $"Channel #{channel} has been deleted.");
|
||||||
});
|
});
|
||||||
@@ -844,6 +841,16 @@ public sealed class AppOrchestrator : IDisposable
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
connection.OnChannelUpdated += channel =>
|
||||||
|
{
|
||||||
|
InvokeUI(() =>
|
||||||
|
{
|
||||||
|
if (channel.IsPublic)
|
||||||
|
_mainWindow.EnsureChannelInList(channel.Name);
|
||||||
|
_mainWindow.SetChannelTopic(channel.Name, channel.Topic);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
connection.OnError += errorMessage =>
|
connection.OnError += errorMessage =>
|
||||||
InvokeUI(() => _mainWindow.ShowError(errorMessage));
|
InvokeUI(() => _mainWindow.ShowError(errorMessage));
|
||||||
|
|
||||||
|
|||||||
@@ -596,6 +596,16 @@ public sealed class MainWindow : Runnable
|
|||||||
RefreshChannelList();
|
RefreshChannelList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a channel from the left panel list.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveChannel(string channelName)
|
||||||
|
{
|
||||||
|
_channelNames.Remove(channelName);
|
||||||
|
_channelTopics.Remove(channelName);
|
||||||
|
RefreshChannelList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the topic for a specific channel.
|
/// Update the topic for a specific channel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EchoHub.Core.Models;
|
||||||
|
|
||||||
|
public class ChannelMembership
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public Guid ChannelId { get; set; }
|
||||||
|
public DateTimeOffset JoinedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
@@ -40,10 +40,17 @@ public class ChannelsController : ControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetChannels([FromQuery] int offset = 0, [FromQuery] int limit = 50)
|
public async Task<IActionResult> GetChannels([FromQuery] int offset = 0, [FromQuery] int limit = 50)
|
||||||
{
|
{
|
||||||
|
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (userIdClaim is null)
|
||||||
|
return Unauthorized(new ErrorResponse("Authentication required."));
|
||||||
|
|
||||||
|
var userId = Guid.Parse(userIdClaim);
|
||||||
offset = Math.Max(0, offset);
|
offset = Math.Max(0, offset);
|
||||||
limit = Math.Clamp(limit, 1, 100);
|
limit = Math.Clamp(limit, 1, 100);
|
||||||
|
|
||||||
var query = _db.Channels.Where(c => c.IsPublic);
|
// Public channels + private channels the user has joined
|
||||||
|
var query = _db.Channels.Where(c =>
|
||||||
|
c.IsPublic || _db.ChannelMemberships.Any(m => m.ChannelId == c.Id && m.UserId == userId));
|
||||||
var total = await query.CountAsync();
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
var channels = await query
|
var channels = await query
|
||||||
@@ -90,6 +97,14 @@ public class ChannelsController : ControllerBase
|
|||||||
};
|
};
|
||||||
|
|
||||||
_db.Channels.Add(channel);
|
_db.Channels.Add(channel);
|
||||||
|
|
||||||
|
// Creator automatically becomes a member
|
||||||
|
_db.ChannelMemberships.Add(new ChannelMembership
|
||||||
|
{
|
||||||
|
UserId = Guid.Parse(userIdClaim),
|
||||||
|
ChannelId = channel.Id,
|
||||||
|
});
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
var dto = new ChannelDto(channel.Id, channel.Name, channel.Topic, channel.IsPublic, 0, channel.CreatedAt);
|
var dto = new ChannelDto(channel.Id, channel.Name, channel.Topic, channel.IsPublic, 0, channel.CreatedAt);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class EchoHubDbContext : DbContext
|
|||||||
public DbSet<Channel> Channels => Set<Channel>();
|
public DbSet<Channel> Channels => Set<Channel>();
|
||||||
public DbSet<Message> Messages => Set<Message>();
|
public DbSet<Message> Messages => Set<Message>();
|
||||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||||
|
public DbSet<ChannelMembership> ChannelMemberships => Set<ChannelMembership>();
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@@ -60,6 +61,23 @@ public class EchoHubDbContext : DbContext
|
|||||||
entity.Property(m => m.AttachmentFileName).HasMaxLength(255);
|
entity.Property(m => m.AttachmentFileName).HasMaxLength(255);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ChannelMembership>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(cm => new { cm.UserId, cm.ChannelId });
|
||||||
|
entity.HasIndex(cm => cm.UserId);
|
||||||
|
entity.HasIndex(cm => cm.ChannelId);
|
||||||
|
|
||||||
|
entity.HasOne<Channel>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(cm => cm.ChannelId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
entity.HasOne<User>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(cm => cm.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<RefreshToken>(entity =>
|
modelBuilder.Entity<RefreshToken>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(r => r.Id);
|
entity.HasKey(r => r.Id);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace EchoHub.Server.Data.Migrations
|
|||||||
table: "Channels",
|
table: "Channels",
|
||||||
type: "INTEGER",
|
type: "INTEGER",
|
||||||
nullable: false,
|
nullable: false,
|
||||||
defaultValue: false);
|
defaultValue: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
+260
@@ -0,0 +1,260 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using EchoHub.Server.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EchoHub.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(EchoHubDbContext))]
|
||||||
|
[Migration("20260219181720_AddChannelMembership")]
|
||||||
|
partial class AddChannelMembership
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.Channel", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("CreatedAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedByUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPublic")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Topic")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Channels");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.ChannelMembership", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("JoinedAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "ChannelId");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("ChannelMemberships");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentFileName")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SenderUsername")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("SentAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId");
|
||||||
|
|
||||||
|
b.HasIndex("SentAt");
|
||||||
|
|
||||||
|
b.ToTable("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("CreatedAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("ExpiresAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("RevokedAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarAscii")
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("CreatedAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBanned")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMuted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("LastSeenAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("MutedUntil")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("NicknameColor")
|
||||||
|
.HasMaxLength(7)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StatusMessage")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.ChannelMembership", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("EchoHub.Core.Models.Channel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("EchoHub.Core.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("EchoHub.Core.Models.Channel", "Channel")
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Channel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("EchoHub.Core.Models.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.Channel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EchoHub.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddChannelMembership : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ChannelMemberships",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
ChannelId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
JoinedAt = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ChannelMemberships", x => new { x.UserId, x.ChannelId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ChannelMemberships_Channels_ChannelId",
|
||||||
|
column: x => x.ChannelId,
|
||||||
|
principalTable: "Channels",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ChannelMemberships_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ChannelMemberships_ChannelId",
|
||||||
|
table: "ChannelMemberships",
|
||||||
|
column: "ChannelId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ChannelMemberships_UserId",
|
||||||
|
table: "ChannelMemberships",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ChannelMemberships");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,26 @@ namespace EchoHub.Server.Data.Migrations
|
|||||||
b.ToTable("Channels");
|
b.ToTable("Channels");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.ChannelMembership", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("JoinedAt")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "ChannelId");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("ChannelMemberships");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -190,6 +210,21 @@ namespace EchoHub.Server.Data.Migrations
|
|||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EchoHub.Core.Models.ChannelMembership", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("EchoHub.Core.Models.Channel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("EchoHub.Core.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("EchoHub.Core.Models.Channel", "Channel")
|
b.HasOne("EchoHub.Core.Models.Channel", "Channel")
|
||||||
|
|||||||
@@ -98,6 +98,19 @@ public class ChatService : IChatService
|
|||||||
if (channel is null)
|
if (channel is null)
|
||||||
return ([], $"Channel '{channelName}' does not exist. Create it first via the channel list.");
|
return ([], $"Channel '{channelName}' does not exist. Create it first via the channel list.");
|
||||||
|
|
||||||
|
// Persist membership so the channel shows in the user's channel list
|
||||||
|
var hasMembership = await db.ChannelMemberships
|
||||||
|
.AnyAsync(m => m.UserId == userId && m.ChannelId == channel.Id);
|
||||||
|
if (!hasMembership)
|
||||||
|
{
|
||||||
|
db.ChannelMemberships.Add(new ChannelMembership
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ChannelId = channel.Id,
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
var isNewJoin = _presenceTracker.JoinChannel(username, channelName);
|
var isNewJoin = _presenceTracker.JoinChannel(username, channelName);
|
||||||
|
|
||||||
if (isNewJoin)
|
if (isNewJoin)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using EchoHub.Core.Constants;
|
||||||
using EchoHub.Server.Data;
|
using EchoHub.Server.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -13,9 +14,24 @@ public static partial class DataMigrationService
|
|||||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>()
|
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>()
|
||||||
.CreateLogger("EchoHub.Server.Setup.DataMigration");
|
.CreateLogger("EchoHub.Server.Setup.DataMigration");
|
||||||
|
|
||||||
|
await EnsureDefaultChannelsPublicAsync(db, logger);
|
||||||
await MigrateAnsiMessagesAsync(db, logger);
|
await MigrateAnsiMessagesAsync(db, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure the #general channel (and any pre-existing channels from before the IsPublic column) are public.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task EnsureDefaultChannelsPublicAsync(EchoHubDbContext db, ILogger logger)
|
||||||
|
{
|
||||||
|
var general = await db.Channels.FirstOrDefaultAsync(c => c.Name == HubConstants.DefaultChannel);
|
||||||
|
if (general is not null && !general.IsPublic)
|
||||||
|
{
|
||||||
|
general.IsPublic = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
logger.LogInformation("Marked #{Channel} as public.", HubConstants.DefaultChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task MigrateAnsiMessagesAsync(EchoHubDbContext db, ILogger logger)
|
private static async Task MigrateAnsiMessagesAsync(EchoHubDbContext db, ILogger logger)
|
||||||
{
|
{
|
||||||
// Load messages that contain the ESC byte (0x1B) — these have legacy ANSI color codes.
|
// Load messages that contain the ESC byte (0x1B) — these have legacy ANSI color codes.
|
||||||
|
|||||||
Reference in New Issue
Block a user