├── .gitignore ├── BasicRedisChat.BLL ├── Base │ └── Service │ │ ├── BaseService.cs │ │ └── Interfaces │ │ └── IBaseService.cs ├── BasicRedisChat.BLL.csproj ├── Components │ └── Main │ │ ├── ChatСomponent │ │ ├── Dtos │ │ │ ├── ChatRoomDto.cs │ │ │ └── ChatRoomMessageDto.cs │ │ ├── Entities │ │ │ ├── ChatRoom.cs │ │ │ └── ChatRoomMessage.cs │ │ └── Services │ │ │ ├── ChatService.cs │ │ │ └── Interfaces │ │ │ └── IChatService.cs │ │ └── UserСomponent │ │ ├── Dtos │ │ ├── UserDto.cs │ │ └── UserLoginDto.cs │ │ ├── Entities │ │ └── User.cs │ │ └── Services │ │ ├── Interfaces │ │ ├── ISecurityService.cs │ │ └── IUserService.cs │ │ ├── SecurityService.cs │ │ └── UserService.cs ├── DbContext │ └── DbInitializer.cs ├── Dtos │ └── BaseDto.cs ├── Helpers │ └── ConvertToDto.cs └── Hubs │ ├── BaseHub.cs │ └── ChatHub.cs ├── BasicRedisChat.Base ├── BasicRedisChat.Base.csproj └── Interfaces │ ├── IService.cs │ └── ISingletonService.cs ├── BasicRedisChat.DAL ├── BasicRedisChat.DAL.csproj └── Entities │ └── BaseEntity.cs ├── BasicRedisChat.sln ├── BasicRedisChat ├── .gitignore ├── BasicRedisChat.csproj ├── Configs │ ├── AutoMapperConfig.cs │ ├── RedisSettings.cs │ └── ServiceAutoConfig.cs ├── Controllers │ ├── AuthController.cs │ ├── Base │ │ └── ApiController.cs │ ├── LinksController.cs │ ├── RoomsController.cs │ └── UsersController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── app.json ├── appsettings.Development.json ├── appsettings.json ├── client │ ├── .gitignore │ ├── README.md │ ├── build │ │ ├── asset-manifest.json │ │ ├── avatars │ │ │ ├── 0.jpg │ │ │ ├── 1.jpg │ │ │ ├── 10.jpg │ │ │ ├── 11.jpg │ │ │ ├── 12.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ ├── 5.jpg │ │ │ ├── 6.jpg │ │ │ ├── 7.jpg │ │ │ ├── 8.jpg │ │ │ └── 9.jpg │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── robots.txt │ │ ├── static │ │ │ ├── css │ │ │ │ ├── 2.150d169a.chunk.css │ │ │ │ ├── 2.150d169a.chunk.css.map │ │ │ │ ├── main.68a2faa8.chunk.css │ │ │ │ └── main.68a2faa8.chunk.css.map │ │ │ └── js │ │ │ │ ├── 2.6efd31e3.chunk.js │ │ │ │ ├── 2.6efd31e3.chunk.js.LICENSE.txt │ │ │ │ ├── 2.6efd31e3.chunk.js.map │ │ │ │ ├── main.040969fe.chunk.js │ │ │ │ ├── main.040969fe.chunk.js.map │ │ │ │ ├── runtime-main.c012fedc.js │ │ │ │ └── runtime-main.c012fedc.js.map │ │ └── welcome-back.png │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── avatars │ │ │ ├── 0.jpg │ │ │ ├── 1.jpg │ │ │ ├── 10.jpg │ │ │ ├── 11.jpg │ │ │ ├── 12.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ ├── 5.jpg │ │ │ ├── 6.jpg │ │ │ ├── 7.jpg │ │ │ ├── 8.jpg │ │ │ └── 9.jpg │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── robots.txt │ │ └── welcome-back.png │ ├── src │ │ ├── App.jsx │ │ ├── api.js │ │ ├── components │ │ │ ├── Chat │ │ │ │ ├── components │ │ │ │ │ ├── ChatList │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── AvatarImage.jsx │ │ │ │ │ │ │ ├── ChatIcon.jsx │ │ │ │ │ │ │ ├── ChatListItem │ │ │ │ │ │ │ │ ├── index.jsx │ │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ │ └── Footer.jsx │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── MessageList │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── ClockIcon.jsx │ │ │ │ │ │ │ ├── InfoMessage.jsx │ │ │ │ │ │ │ ├── MessagesLoading.jsx │ │ │ │ │ │ │ ├── NoMessages.jsx │ │ │ │ │ │ │ ├── ReceiverMessage.jsx │ │ │ │ │ │ │ └── SenderMessage.jsx │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── OnlineIndicator.jsx │ │ │ │ │ └── TypingArea.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── use-chat-handlers.js │ │ │ ├── LoadingScreen.jsx │ │ │ ├── Login │ │ │ │ ├── index.jsx │ │ │ │ └── style.css │ │ │ ├── Logo.jsx │ │ │ └── Navbar.jsx │ │ ├── hooks.js │ │ ├── index.jsx │ │ ├── state.js │ │ ├── styles │ │ │ ├── font-face.css │ │ │ ├── style-overrides.css │ │ │ └── style.css │ │ ├── use-socket.js │ │ └── utils.js │ └── yarn.lock └── repo.json ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── docker-compose.yaml ├── docs ├── YTThumbnail.png ├── screenshot000.png └── screenshot001.png ├── heroku.yml ├── images └── app_preview_image.png └── marketplace.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # Rider 12 | .idea 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | msbuild.log 33 | msbuild.err 34 | msbuild.wrn 35 | 36 | # Visual Studio 2015 37 | .vs/ 38 | 39 | !client/build -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Base/Service/BaseService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Base.Service.Interfaces; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Serialization; 4 | using StackExchange.Redis; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | 11 | namespace BasicRedisChat.BLL.Base.Service 12 | { 13 | public abstract class BaseService : IBaseService 14 | { 15 | protected readonly IDatabase _database; 16 | protected readonly IConnectionMultiplexer _redis; 17 | 18 | public BaseService(IConnectionMultiplexer redis) 19 | { 20 | _redis = redis; 21 | _database = redis.GetDatabase(); 22 | } 23 | 24 | protected async Task PublishMessage(string type, T data) 25 | { 26 | // That's a very quick and dirty way to handle the json type serialization... 27 | var jsonData = JsonConvert.SerializeObject(data, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); 28 | //var jsonData = JsonConvert.DeserializeObject(dataString); 29 | 30 | var pubSubMessage = new PubSubMessage() 31 | { 32 | Type = type, 33 | Data = jsonData 34 | }; 35 | 36 | await PublishMessage(pubSubMessage); 37 | } 38 | 39 | private async Task PublishMessage(PubSubMessage pubSubMessage) 40 | { 41 | await _database.PublishAsync("MESSAGES", JsonConvert.SerializeObject(pubSubMessage)); 42 | } 43 | } 44 | 45 | public class PubSubMessage 46 | { 47 | public string Type { get; set; } 48 | public string Data { get; set; } 49 | public string ServerId { get; set; } = "123"; 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Base/Service/Interfaces/IBaseService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.Base.Interfaces; 2 | using StackExchange.Redis; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BasicRedisChat.BLL.Base.Service.Interfaces 9 | { 10 | public interface IBaseService : IService 11 | { 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/BasicRedisChat.BLL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/ChatСomponent/Dtos/ChatRoomDto.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Dtos; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace BasicRedisChat.BLL.Components.Main.ChatСomponent.Dtos 8 | { 9 | public class ChatRoomDto : BaseDto 10 | { 11 | public string Id { get; set; } 12 | public IEnumerable Names { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/ChatСomponent/Dtos/ChatRoomMessageDto.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Dtos; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace BasicRedisChat.BLL.Components.Main.ChatСomponent.Dtos 8 | { 9 | public class ChatRoomMessageDto : BaseDto 10 | { 11 | public string From { get; set; } 12 | public int Date { get; set; } 13 | public string Message { get; set; } 14 | public string RoomId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/ChatСomponent/Entities/ChatRoom.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.DAL.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Text; 6 | 7 | namespace BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities 8 | { 9 | public class ChatRoom : BaseEntity 10 | { 11 | public string Id { get; set; } 12 | public IEnumerable Names { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/ChatСomponent/Entities/ChatRoomMessage.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.DAL.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Text; 6 | 7 | namespace BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities 8 | { 9 | public class ChatRoomMessage : BaseEntity 10 | { 11 | public string From { get; set; } 12 | public int Date { get; set; } 13 | public string Message { get; set; } 14 | public string RoomId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/ChatСomponent/Services/ChatService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Base.Service; 2 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities; 3 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Services.Interfaces; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 5 | using Newtonsoft.Json; 6 | using StackExchange.Redis; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | 11 | namespace BasicRedisChat.BLL.Components.Main.ChatСomponent.Services 12 | { 13 | public class ChatService : BaseService, IChatService 14 | { 15 | public ChatService(IConnectionMultiplexer redis) : base(redis) 16 | { 17 | } 18 | 19 | public async Task> GetMessages(string roomId = "0", int offset = 0, int size = 50) 20 | { 21 | var roomKey = $"room:{roomId}"; 22 | var roomExists = await _database.KeyExistsAsync(roomKey); 23 | var messages = new List(); 24 | 25 | if (!roomExists) 26 | { 27 | return messages; 28 | } 29 | else 30 | { 31 | var values = await _database.SortedSetRangeByRankAsync(roomKey, offset, offset + size, Order.Descending); 32 | 33 | foreach (var valueRedisVal in values) 34 | { 35 | var value = valueRedisVal.ToString(); 36 | try 37 | { 38 | messages.Add(JsonConvert.DeserializeObject(value)); 39 | } 40 | catch (System.Text.Json.JsonException) 41 | { 42 | // Console.WriteLine($"Couldn't deserialize json: {value}"); 43 | } 44 | } 45 | return messages; 46 | } 47 | } 48 | 49 | public async Task> GetRooms(int userId = 0) 50 | { 51 | var roomIds = await _database.SetMembersAsync($"user:{userId}:rooms"); 52 | var rooms = new List(); 53 | foreach (var roomIdRedisValue in roomIds) 54 | { 55 | var roomId = roomIdRedisValue.ToString(); 56 | var name = await _database.StringGetAsync($"room:{roomId}:name"); 57 | if (name.IsNullOrEmpty) 58 | { 59 | // It's a room without a name, likey the one with private messages 60 | var roomExists = await _database.KeyExistsAsync($"room:{roomId}"); 61 | if (!roomExists) 62 | { 63 | continue; 64 | } 65 | 66 | var userIds = roomId.Split(':'); 67 | if (userIds.Length != 2) 68 | { 69 | throw new Exception("You don't have access to this room"); 70 | } 71 | 72 | rooms.Add(new ChatRoom() 73 | { 74 | Id = roomId, 75 | Names = new List() { 76 | (await _database.HashGetAsync($"user:{userIds[0]}", "username")).ToString(), 77 | (await _database.HashGetAsync($"user:{userIds[1]}", "username")).ToString(), 78 | } 79 | }); 80 | } 81 | else 82 | { 83 | rooms.Add(new ChatRoom() 84 | { 85 | Id = roomId, 86 | Names = new List() { 87 | name.ToString() 88 | } 89 | }); 90 | } 91 | } 92 | return rooms; 93 | } 94 | 95 | public async Task SendMessage(UserDto user, ChatRoomMessage message) 96 | { 97 | await _database.SetAddAsync("online_users", message.From); 98 | var roomKey = $"room:{message.RoomId}"; 99 | await _database.SortedSetAddAsync(roomKey, JsonConvert.SerializeObject(message), (double)message.Date); 100 | await PublishMessage("message", message); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/ChatСomponent/Services/Interfaces/IChatService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Base.Service.Interfaces; 2 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Dtos; 3 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace BasicRedisChat.BLL.Components.Main.ChatСomponent.Services.Interfaces 11 | { 12 | public interface IChatService : IBaseService 13 | { 14 | Task> GetRooms(int userId = 0); 15 | Task> GetMessages(string roomId = "0", int offset = 0, int size = 50); 16 | Task SendMessage(UserDto user, ChatRoomMessage message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Dtos/UserDto.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Dtos; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos 8 | { 9 | public class UserDto : BaseDto 10 | { 11 | public int Id { get; set; } 12 | public string Username { get; set; } 13 | public bool Online { get; set; } = false; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Dtos/UserLoginDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos 7 | { 8 | public class UserLoginDto 9 | { 10 | [Required(AllowEmptyStrings = false)] 11 | public string Username { get; set; } 12 | 13 | [Required(AllowEmptyStrings = false)] 14 | public string Password { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Entities/User.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.DAL.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Entities 7 | { 8 | public class User : BaseEntity 9 | { 10 | public int Id { get; set; } 11 | public string Username { get; set; } 12 | public bool Online { get; set; } = false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Services/Interfaces/ISecurityService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.Base.Interfaces; 2 | using BasicRedisChat.BLL.Base.Service.Interfaces; 3 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 5 | using Microsoft.AspNetCore.Http; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces 12 | { 13 | public interface ISecurityService : IBaseService 14 | { 15 | Task Login(UserLoginDto userLoginDto); 16 | Task Logout(HttpContext httpContext); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Services/Interfaces/IUserService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.Base.Interfaces; 2 | using BasicRedisChat.BLL.Base.Service.Interfaces; 3 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces 11 | { 12 | public interface IUserService : IBaseService 13 | { 14 | Task> Get(int[] ids); 15 | Task> GetOnline(); 16 | 17 | Task OnStartSession(UserDto user); 18 | Task OnStopSession(UserDto user); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Services/SecurityService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Base.Service; 2 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 3 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces; 5 | using Microsoft.AspNetCore.Http; 6 | using StackExchange.Redis; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Services 14 | { 15 | public class SecurityService : BaseService, ISecurityService 16 | { 17 | public SecurityService(IConnectionMultiplexer redis) : base(redis) 18 | { 19 | } 20 | 21 | public async Task Login(UserLoginDto userLoginDto) 22 | { 23 | var usernameKey = $"username:{userLoginDto.Username}"; 24 | var userExists = await _database.KeyExistsAsync(usernameKey); 25 | if (userExists) 26 | { 27 | var userKey = (await _database.StringGetAsync(usernameKey)).ToString(); 28 | var userId = int.Parse(userKey.Split(':').Last()); 29 | return new User() 30 | { 31 | Username = userLoginDto.Username, 32 | Id = userId, 33 | Online = true 34 | }; 35 | } else 36 | { 37 | return null; 38 | } 39 | } 40 | 41 | public async Task Logout(HttpContext httpContext) 42 | { 43 | httpContext.Session.Remove("user"); 44 | await httpContext.Session.CommitAsync(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Components/Main/UserСomponent/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Base.Service; 2 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 3 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces; 5 | using StackExchange.Redis; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Threading.Tasks; 11 | 12 | namespace BasicRedisChat.BLL.Components.Main.UserСomponent.Services 13 | { 14 | public class UserService : BaseService, IUserService 15 | { 16 | public UserService(IConnectionMultiplexer redis) : base(redis) 17 | { 18 | } 19 | 20 | public async Task> Get(int[] ids) 21 | { 22 | var users = new Dictionary(); 23 | foreach (var id in ids) 24 | { 25 | users.Add(id.ToString(), new UserDto() 26 | { 27 | Id = id, 28 | Username = await _database.HashGetAsync($"user:{id}", "username"), 29 | Online = await _database.SetContainsAsync("online_users", id.ToString()) 30 | }); 31 | } 32 | return users; 33 | } 34 | 35 | public async Task> GetOnline() 36 | { 37 | var onlineIds = await _database.SetMembersAsync("online_users"); 38 | var users = new Dictionary(); 39 | foreach (var onlineIdRedisValue in onlineIds) 40 | { 41 | var onlineId = onlineIdRedisValue.ToString(); 42 | var user = await _database.HashGetAsync($"user:{onlineId}", "username"); 43 | users.Add(onlineId, new UserDto() 44 | { 45 | Id = Int32.Parse(onlineId), 46 | Username = user.ToString(), 47 | Online = true 48 | }); 49 | } 50 | 51 | return users; 52 | } 53 | 54 | public async Task OnStartSession(UserDto user) 55 | { 56 | await _database.SetAddAsync("online_users", user.Id); 57 | user.Online = true; 58 | await PublishMessage("user.connected", user.Username); 59 | } 60 | 61 | public async Task OnStopSession(UserDto user) 62 | { 63 | await _database.SetRemoveAsync("online_users", user.Id); 64 | user.Online = false; 65 | await PublishMessage("user.disconnected", user.Username); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/DbContext/DbInitializer.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities; 2 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using StackExchange.Redis; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Threading.Tasks; 11 | 12 | namespace BasicRedisChat.BLL.DbContext 13 | { 14 | public static class DbInitializer 15 | { 16 | public static async Task Seed(IServiceScope serviceScope) 17 | { 18 | var redis = serviceScope.ServiceProvider.GetService(); 19 | var redisDatabase = redis.GetDatabase(); 20 | // We store a counter for the total users and increment it on each register 21 | var totalUsersKeyExist = await redisDatabase.KeyExistsAsync("total_users"); 22 | if (!totalUsersKeyExist) 23 | { 24 | // This counter is used for the id 25 | await redisDatabase.StringSetAsync("total_users", 0); 26 | // Some rooms have pre-defined names. When the clients attempts to fetch a room, an additional lookup 27 | // is handled to resolve the name. 28 | // Rooms with private messages don't have a name 29 | await redisDatabase.StringSetAsync("room:0:name", "General"); 30 | 31 | // Create demo data with the default users 32 | { 33 | var rnd = new Random(); 34 | Func rand = () => rnd.NextDouble(); 35 | Func getTimestamp = () => (int)DateTimeOffset.Now.ToUnixTimeSeconds(); 36 | 37 | var demoPassword = "password123"; 38 | var demoUsers = new List() { "Pablo", "Joe", "Mary", "Alex" }; 39 | 40 | var greetings = new List() { "Hello", "Hi", "Yo", "Hola" }; 41 | 42 | var messages = new List() { 43 | "Hello!", 44 | "Hi, How are you? What about our next meeting?", 45 | "Yeah everything is fine", 46 | "Next meeting tomorrow 10.00AM", 47 | "Wow that's great" 48 | }; 49 | Func getGreeting = () => greetings[(int)Math.Floor(rand() * greetings.Count)]; 50 | var addMessage = new Func(async (string roomId, string fromId, string content, int timeStamp) => 51 | { 52 | var roomKey = $"room:{roomId}"; 53 | var message = new ChatRoomMessage() 54 | { 55 | From = fromId, 56 | Date = timeStamp, 57 | Message = content, 58 | RoomId = roomId 59 | }; 60 | await redisDatabase.SortedSetAddAsync(roomKey, JsonSerializer.Serialize(message), message.Date); 61 | }); 62 | 63 | var createUser = new Func>(async (string username, string password) => 64 | { 65 | var usernameKey = $"username:{username}"; 66 | // Yeah, bcrypt generally ins't used in .NET, this one is mainly added to be compatible with Node and Python demo servers. 67 | var hashedPassword = BCrypt.Net.BCrypt.HashPassword(password); 68 | var nextId = await redisDatabase.StringIncrementAsync("total_users"); 69 | var userKey = $"user:{nextId}"; 70 | await redisDatabase.StringSetAsync(usernameKey, userKey); 71 | await redisDatabase.HashSetAsync(userKey, new HashEntry[] { 72 | new HashEntry("username", username), 73 | new HashEntry("password", hashedPassword) 74 | }); 75 | 76 | await redisDatabase.SetAddAsync($"user:{nextId}:rooms", "0"); 77 | 78 | return new User() 79 | { 80 | Id = (int)nextId, 81 | Username = username, 82 | Online = false 83 | }; 84 | }); 85 | 86 | var getPrivateRoomId = new Func((user1, user2) => 87 | { 88 | var minUserId = user1 > user2 ? user2 : user1; 89 | var maxUserId = user1 > user2 ? user1 : user2; 90 | return $"{minUserId}:{maxUserId}"; 91 | }); 92 | 93 | var createPrivateRoom = new Func>(async (user1, user2) => 94 | { 95 | var roomId = getPrivateRoomId(user1, user2); 96 | 97 | await redisDatabase.SetAddAsync($"user:{user1}:rooms", roomId); 98 | await redisDatabase.SetAddAsync($"user:{user2}:rooms", roomId); 99 | 100 | return new ChatRoom() 101 | { 102 | Id = roomId, 103 | Names = new List{ 104 | (await redisDatabase.HashGetAsync($"user:{user1}", "username")).ToString(), 105 | (await redisDatabase.HashGetAsync($"user:{user2}", "username")).ToString(), 106 | } 107 | }; 108 | }); 109 | 110 | 111 | var users = new List(); 112 | // For each name create a user. 113 | foreach (var demoUser in demoUsers) 114 | { 115 | var user = await createUser(demoUser, demoPassword); 116 | // This one should go to the session 117 | users.Add(user); 118 | } 119 | 120 | var rooms = new Dictionary(); 121 | foreach (var user in users) 122 | { 123 | var otherUsers = users.Where(x => x.Id != user.Id); 124 | foreach (var otherUser in otherUsers) 125 | { 126 | var privateRoomId = getPrivateRoomId(user.Id, otherUser.Id); 127 | ChatRoom room = null; 128 | if (!rooms.ContainsKey(privateRoomId)) 129 | { 130 | room = await createPrivateRoom(user.Id, otherUser.Id); 131 | rooms.Add(privateRoomId, room); 132 | } 133 | else 134 | { 135 | room = rooms[privateRoomId]; 136 | } 137 | await addMessage(privateRoomId, otherUser.Id.ToString(), getGreeting(), (int)(getTimestamp() - rand() * 222)); 138 | } 139 | } 140 | var getRandomUserId = new Func(() => users[(int)Math.Floor(users.Count * rand())].Id); 141 | for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++) 142 | { 143 | await addMessage("0", getRandomUserId().ToString(), messages[messageIndex], getTimestamp() - ((messages.Count - messageIndex) * 200)); 144 | } 145 | } 146 | 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Dtos/BaseDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BasicRedisChat.BLL.Dtos 6 | { 7 | public class BaseDto 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Helpers/ConvertToDto.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using BasicRedisChat.BLL.Dtos; 3 | using BasicRedisChat.DAL.Entities; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace BasicRedisChat.BLL.Helpers 9 | { 10 | public static class ConvertToDto 11 | { 12 | public static IMapper Mapper = null; 13 | 14 | public static TDto ToDto(this BaseEntity obj) where TDto : BaseDto 15 | { 16 | if (Mapper == null) 17 | { 18 | throw new System.Exception("Incorrect initialization for AutoMapper Helper"); 19 | } 20 | return Mapper.Map(obj); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Hubs/BaseHub.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 2 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 3 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.SignalR; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Text.Json; 11 | using System.Threading.Tasks; 12 | 13 | namespace BasicRedisChat.BLL.Hubs 14 | { 15 | public class BaseHub : Hub 16 | { 17 | 18 | //private readonly IParserManagerService parserManagerService; 19 | private readonly IUserService userService; 20 | 21 | public BaseHub(IUserService userService) 22 | { 23 | this.userService = userService; 24 | } 25 | 26 | public override async Task OnConnectedAsync() 27 | { 28 | 29 | var httpContext = Context.GetHttpContext(); 30 | await httpContext.Session.LoadAsync(); 31 | var userStr = httpContext.Session.GetString("user"); 32 | if (!string.IsNullOrEmpty(userStr)) 33 | { 34 | var user = JsonSerializer.Deserialize(userStr); 35 | 36 | await userService.OnStartSession(user); 37 | 38 | } 39 | else 40 | { 41 | await OnDisconnectedAsync(new Exception("Not Authorized")); 42 | } 43 | 44 | await base.OnConnectedAsync(); 45 | } 46 | 47 | public override async Task OnDisconnectedAsync(Exception exception) 48 | { 49 | var httpContext = Context.GetHttpContext(); 50 | await httpContext.Session.LoadAsync(); 51 | var userStr = httpContext.Session.GetString("user"); 52 | if (!string.IsNullOrEmpty(userStr)) 53 | { 54 | var user = JsonSerializer.Deserialize(userStr); 55 | 56 | await userService.OnStopSession(user); 57 | 58 | } 59 | 60 | await base.OnDisconnectedAsync(exception); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /BasicRedisChat.BLL/Hubs/ChatHub.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Dtos; 2 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities; 3 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Services.Interfaces; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 5 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 6 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces; 7 | using Newtonsoft.Json; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | using System.Text.Json; 12 | using System.Threading.Tasks; 13 | 14 | namespace BasicRedisChat.BLL.Hubs 15 | { 16 | public class ChatHub : BaseHub 17 | { 18 | private readonly IChatService chatService; 19 | public ChatHub(IUserService userService, IChatService chatService) : base(userService) 20 | { 21 | this.chatService = chatService; 22 | } 23 | 24 | public async Task SendMessage(string userString, string messageString) 25 | { 26 | var message = JsonConvert.DeserializeObject(messageString); 27 | 28 | var user = JsonConvert.DeserializeObject(userString); 29 | 30 | await chatService.SendMessage(user, message); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BasicRedisChat.Base/BasicRedisChat.Base.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BasicRedisChat.Base/Interfaces/IService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BasicRedisChat.Base.Interfaces 6 | { 7 | public interface IService 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BasicRedisChat.Base/Interfaces/ISingletonService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BasicRedisChat.Base.Interfaces 6 | { 7 | public interface ISingletonService 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BasicRedisChat.DAL/BasicRedisChat.DAL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /BasicRedisChat.DAL/Entities/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BasicRedisChat.DAL.Entities 6 | { 7 | public abstract class BaseEntity 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BasicRedisChat.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicRedisChat", "BasicRedisChat\BasicRedisChat.csproj", "{1DC0794D-A003-4791-B069-E0D21A6F7918}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicRedisChat.Base", "BasicRedisChat.Base\BasicRedisChat.Base.csproj", "{CFCAA2E3-4FA5-4677-A507-662BE4A2EE74}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicRedisChat.DAL", "BasicRedisChat.DAL\BasicRedisChat.DAL.csproj", "{CB4D9FAB-A25E-40F5-A182-C1CA4F71D62A}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicRedisChat.BLL", "BasicRedisChat.BLL\BasicRedisChat.BLL.csproj", "{67346D11-6557-4F77-9F0C-139F1F62EBA4}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {1DC0794D-A003-4791-B069-E0D21A6F7918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {1DC0794D-A003-4791-B069-E0D21A6F7918}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {1DC0794D-A003-4791-B069-E0D21A6F7918}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {1DC0794D-A003-4791-B069-E0D21A6F7918}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {CFCAA2E3-4FA5-4677-A507-662BE4A2EE74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {CFCAA2E3-4FA5-4677-A507-662BE4A2EE74}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {CFCAA2E3-4FA5-4677-A507-662BE4A2EE74}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {CFCAA2E3-4FA5-4677-A507-662BE4A2EE74}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {CB4D9FAB-A25E-40F5-A182-C1CA4F71D62A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {CB4D9FAB-A25E-40F5-A182-C1CA4F71D62A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {CB4D9FAB-A25E-40F5-A182-C1CA4F71D62A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {CB4D9FAB-A25E-40F5-A182-C1CA4F71D62A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {67346D11-6557-4F77-9F0C-139F1F62EBA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {67346D11-6557-4F77-9F0C-139F1F62EBA4}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {67346D11-6557-4F77-9F0C-139F1F62EBA4}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {67346D11-6557-4F77-9F0C-139F1F62EBA4}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {A964BDB7-F007-4067-98D9-0E319E531EE7} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /BasicRedisChat/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # Rider 12 | .idea 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | */[Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | msbuild.log 33 | msbuild.err 34 | msbuild.wrn 35 | 36 | # Visual Studio 2015 37 | .vs/ 38 | 39 | !client/build -------------------------------------------------------------------------------- /BasicRedisChat/BasicRedisChat.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | true 18 | $(NoWarn);1591 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /BasicRedisChat/Configs/AutoMapperConfig.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Dtos; 3 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Entities; 4 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 5 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace BasicRedisChat.Configs 12 | { 13 | public class AutoMapperConfig : Profile 14 | { 15 | public AutoMapperConfig() 16 | { 17 | CreateMap(); 18 | CreateMap(); 19 | CreateMap(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BasicRedisChat/Configs/RedisSettings.cs: -------------------------------------------------------------------------------- 1 | namespace BasicRedisChat.Configs 2 | { 3 | public class RedisSettings 4 | { 5 | public string Url { get; set; } 6 | public string Port { get; set; } 7 | public string Password { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BasicRedisChat/Configs/ServiceAutoConfig.cs: -------------------------------------------------------------------------------- 1 | using BasicRedisChat.Base.Interfaces; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace BasicRedisChat.Configs 7 | { 8 | public class ServiceAutoConfig 9 | { 10 | public static void Configure(IServiceCollection services) 11 | { 12 | var service = typeof(IService); 13 | var singletonService = typeof(ISingletonService); 14 | var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes()).Where(p => (service.IsAssignableFrom(p) || singletonService.IsAssignableFrom(p)) && p.IsClass && !p.IsAbstract).ToList(); 15 | types.ForEach(c => 16 | { 17 | var originInterfaces = c.GetInterfaces(); 18 | var isSingleton = originInterfaces.Any(i => singletonService.IsAssignableFrom(i)); 19 | var interfaces = originInterfaces.Where(x => 20 | x.Name != service.Name || 21 | x.Name != singletonService.Name 22 | ).ToList(); 23 | 24 | interfaces.ForEach(i => 25 | { 26 | if (!isSingleton) 27 | { 28 | services.AddTransient(i, c); 29 | } 30 | else 31 | { 32 | services.AddSingleton(i, c); 33 | } 34 | }); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BasicRedisChat/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Http; 6 | using System.ComponentModel.DataAnnotations; 7 | using System.Text.Json; 8 | using StackExchange.Redis; 9 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces; 10 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 11 | using BasicRedisChat.Controllers.Base; 12 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 13 | using BasicRedisChat.BLL.Helpers; 14 | 15 | namespace BasicRedisChat.Controllers 16 | { 17 | public class AuthController : ApiController 18 | { 19 | private readonly ISecurityService securityService; 20 | 21 | public AuthController(ISecurityService securityService) 22 | { 23 | this.securityService = securityService; 24 | } 25 | 26 | public class LoginData 27 | { 28 | [Required] 29 | public string username { get; set; } 30 | [Required] 31 | public string password { get; set; } 32 | } 33 | 34 | /// 35 | /// Create user session by username and password. 36 | /// 37 | [HttpPost("login")] 38 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDto))] 39 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 40 | public async Task Login(UserLoginDto userLoginDto) 41 | { 42 | 43 | var user = await securityService.Login(userLoginDto); 44 | if (user == null) 45 | { 46 | return Unauthorized(); 47 | } 48 | 49 | var res = user.ToDto(); 50 | 51 | await HttpContext.Session.LoadAsync(); 52 | 53 | var userString = JsonSerializer.Serialize(res); 54 | HttpContext.Session.SetString("user", userString); 55 | await HttpContext.Session.CommitAsync(); 56 | 57 | return Ok(res); 58 | } 59 | 60 | /// 61 | /// Dispose the user session. 62 | /// 63 | [HttpPost("logout")] 64 | public async Task Logout() 65 | { 66 | await securityService.Logout(HttpContext); 67 | return Ok(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BasicRedisChat/Controllers/Base/ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace BasicRedisChat.Controllers.Base 8 | { 9 | [ApiController] 10 | [Route("[controller]")] 11 | public class ApiController : ControllerBase 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BasicRedisChat/Controllers/LinksController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.IO; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | 6 | namespace BasicRedisChat.Controllers 7 | { 8 | [ApiController] 9 | [Route("[controller]")] 10 | public class LinksController 11 | { 12 | public class LinksObject 13 | { 14 | public string github { get; set; } 15 | } 16 | 17 | /// 18 | /// This one outputs the url this demo is hosted at, for specifying the GitHub link url. 19 | /// 20 | [HttpGet] 21 | public async Task Get() 22 | { 23 | return JsonSerializer.Deserialize(await File.ReadAllTextAsync("repo.json")); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BasicRedisChat/Controllers/RoomsController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using StackExchange.Redis; 6 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Services.Interfaces; 7 | using BasicRedisChat.Controllers.Base; 8 | using System.Linq; 9 | using BasicRedisChat.BLL.Helpers; 10 | using BasicRedisChat.BLL.Components.Main.ChatСomponent.Dtos; 11 | 12 | namespace BasicRedisChat.Controllers 13 | { 14 | /// 15 | /// Used to retrieve the room-related data. 16 | /// 17 | public class RoomsController : ApiController 18 | { 19 | private readonly IChatService _chatService; 20 | 21 | public RoomsController(IChatService chatService) 22 | { 23 | _chatService = chatService; 24 | } 25 | 26 | /// 27 | /// Get rooms for specific user id. 28 | /// 29 | [HttpGet("user/{userId}")] 30 | public async Task GetRoom(int userId = 0) 31 | { 32 | var rooms = await _chatService.GetRooms(userId); 33 | return Ok(rooms.Select(x => x.ToDto())); 34 | } 35 | 36 | /// 37 | /// Get Messages. 38 | /// 39 | [HttpGet("messages/{roomId}")] 40 | public async Task GetMessages(string roomId = "0", int offset = 0, int size = 50) 41 | { 42 | var messages = await _chatService.GetMessages(roomId, offset, size); 43 | return Ok(messages.Select(x => x.ToDto())); 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /BasicRedisChat/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using StackExchange.Redis; 8 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Entities; 9 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Services.Interfaces; 10 | using BasicRedisChat.Controllers.Base; 11 | using BasicRedisChat.BLL.Components.Main.UserСomponent.Dtos; 12 | 13 | namespace BasicRedisChat.Controllers 14 | { 15 | /// 16 | /// This controller handles user state. We don't apploy complete authorization with routes protection. 17 | /// The session simply stores the basic user data which is used for chat communication 18 | /// 19 | [ApiController] 20 | [Route("[controller]")] 21 | public class UsersController : ApiController 22 | { 23 | private IUserService userService; 24 | 25 | public UsersController(IUserService userService) 26 | { 27 | this.userService = userService; 28 | } 29 | 30 | /// 31 | /// Retrieve the user info based on ids sent. 32 | /// 33 | [HttpGet] 34 | public async Task> Get([FromQuery(Name = "ids[]")] int[] ids) 35 | { 36 | return await userService.Get(ids); 37 | } 38 | 39 | /// 40 | /// Check which users are online. 41 | /// 42 | [HttpGet("online")] 43 | public async Task> GetOnline() 44 | { 45 | return await userService.GetOnline(); 46 | } 47 | 48 | /// 49 | /// The request the client sends to check if it has the user is cached. 50 | /// 51 | [HttpGet("me")] 52 | public async Task GetMe() 53 | { 54 | 55 | await HttpContext.Session.LoadAsync(); 56 | 57 | string userString = HttpContext.Session.GetString("user"); 58 | 59 | if (userString != null && userString != "") 60 | { 61 | var user = JsonSerializer.Deserialize(userString); 62 | if (user != null) 63 | { 64 | return user; 65 | } 66 | } 67 | 68 | return null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /BasicRedisChat/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace BasicRedisChat 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) 15 | { 16 | // Accept the PORT environment variable to enable Cloud Run/Heroku support. 17 | var customPort = Environment.GetEnvironmentVariable("PORT"); 18 | if (customPort != null) 19 | { 20 | string url = String.Concat("http://0.0.0.0:", customPort); 21 | 22 | return Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup().UseUrls(url); 26 | }); 27 | } 28 | else 29 | { 30 | 31 | return Host.CreateDefaultBuilder(args) 32 | .ConfigureWebHostDefaults(webBuilder => 33 | { 34 | webBuilder.UseStartup(); 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BasicRedisChat/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:1067", 8 | "sslPort": 44381 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "BasicRedisChat": { 20 | "commandName": "Project", 21 | "dotnetRunMessages": "true", 22 | "launchBrowser": true, 23 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BasicRedisChat/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using BasicRedisChat.BLL.Base.Service; 3 | using BasicRedisChat.BLL.DbContext; 4 | using BasicRedisChat.BLL.Helpers; 5 | using BasicRedisChat.BLL.Hubs; 6 | using BasicRedisChat.Configs; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.DataProtection; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.SignalR; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.FileProviders; 15 | using Microsoft.Extensions.Hosting; 16 | using Microsoft.OpenApi.Models; 17 | using Newtonsoft.Json; 18 | using StackExchange.Redis; 19 | using System; 20 | using System.IO; 21 | using System.Reflection; 22 | 23 | namespace BasicRedisChat 24 | { 25 | public class Startup 26 | { 27 | public Startup(IConfiguration configuration) 28 | { 29 | Configuration = configuration; 30 | } 31 | 32 | public IConfiguration Configuration { get; } 33 | 34 | public void ConfigureServices(IServiceCollection services) 35 | { 36 | 37 | 38 | services.AddControllers(); 39 | services.AddSwaggerGen(c => 40 | { 41 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "BasicRedisChat", Version = "v1" }); 42 | var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 43 | var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); 44 | c.IncludeXmlComments(xmlPath); 45 | }); 46 | 47 | ConnectionMultiplexer redis = null; 48 | string redisConnectionUrl = null; 49 | { 50 | var redisSettings = new RedisSettings(); 51 | ConfigurationBinder.Bind(Configuration.GetSection("RedisSettings"), redisSettings); 52 | 53 | if (redisSettings != null) 54 | { 55 | redisConnectionUrl = $"{redisSettings.Url}:{redisSettings.Port},password={redisSettings.Password}"; 56 | } 57 | else 58 | { 59 | var redisEndpointUrl = (Environment.GetEnvironmentVariable("REDIS_ENDPOINT_URL") ?? "127.0.0.1:6379").Split(':'); 60 | var redisHost = redisEndpointUrl[0]; 61 | var redisPort = redisEndpointUrl[1]; 62 | 63 | var redisPassword = Environment.GetEnvironmentVariable("REDIS_PASSWORD"); 64 | if (redisPassword != null) 65 | { 66 | redisConnectionUrl = $"{redisHost},password={redisPassword}"; 67 | } 68 | else 69 | { 70 | redisConnectionUrl = $"{redisHost}:{redisPort}"; 71 | } 72 | } 73 | 74 | redis = ConnectionMultiplexer.Connect(redisConnectionUrl); 75 | services.AddSingleton(redis); 76 | } 77 | 78 | services 79 | .AddDataProtection() 80 | .PersistKeysToStackExchangeRedis(redis, "DataProtectionKeys"); 81 | 82 | services.AddStackExchangeRedisCache(option => 83 | { 84 | option.Configuration = redisConnectionUrl; 85 | option.InstanceName = "RedisInstance"; 86 | }); 87 | 88 | services.AddSession(options => 89 | { 90 | options.IdleTimeout = TimeSpan.FromMinutes(30); 91 | options.Cookie.Name = "AppTest"; 92 | }); 93 | 94 | Assembly.Load("BasicRedisChat.BLL"); 95 | ServiceAutoConfig.Configure(services); 96 | services.AddAutoMapper(typeof(Startup)); 97 | 98 | services.AddHttpContextAccessor(); 99 | 100 | services.AddSingleton(new PhysicalFileProvider(Directory.GetCurrentDirectory())); 101 | 102 | services.AddSignalR(); 103 | 104 | } 105 | 106 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 107 | { 108 | if (env.IsDevelopment()) 109 | { 110 | app.UseDeveloperExceptionPage(); 111 | app.UseSwagger(); 112 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "dotnet_rest v1")); 113 | } 114 | 115 | app.UseRouting(); 116 | 117 | app.UseAuthorization(); 118 | 119 | app.UseSession(); 120 | 121 | app.Map(new PathString(""), client => 122 | { 123 | var clientPath = Path.Combine(Directory.GetCurrentDirectory(), "./client/build"); 124 | StaticFileOptions clientAppDist = new StaticFileOptions() 125 | { 126 | FileProvider = new PhysicalFileProvider(clientPath) 127 | }; 128 | client.UseSpaStaticFiles(clientAppDist); 129 | client.UseSpa(spa => { spa.Options.DefaultPageStaticFileOptions = clientAppDist; }); 130 | 131 | app.UseEndpoints(endpoints => 132 | { 133 | endpoints.MapHub("/chat"); 134 | endpoints.MapControllers(); 135 | }); 136 | }); 137 | 138 | 139 | IHubContext chatHab = null; 140 | IConnectionMultiplexer redis = null; 141 | using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) 142 | { 143 | DbInitializer.Seed(serviceScope).Wait(); 144 | ConvertToDto.Mapper = serviceScope.ServiceProvider.GetService(); 145 | chatHab = serviceScope.ServiceProvider.GetService>(); 146 | redis = serviceScope.ServiceProvider.GetService(); 147 | } 148 | 149 | var channel = redis.GetSubscriber().Subscribe("MESSAGES"); 150 | channel.OnMessage(async message => 151 | { 152 | try 153 | { 154 | var mess = JsonConvert.DeserializeObject(message.Message.ToString()); 155 | await chatHab.Clients.All.SendAsync(mess.Type, mess.Data); 156 | } 157 | catch (Exception e) 158 | { 159 | Console.WriteLine($"Error: {e} "); 160 | } 161 | }); 162 | 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /BasicRedisChat/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic ASP.NET Core Redis chat", 3 | "env": { 4 | "REDIS_ENDPOINT_URL": { 5 | "description": "A Redis cloud endpoint URL.", 6 | "required": true 7 | }, 8 | "REDIS_PASSWORD": { 9 | "description": "A Redis password.", 10 | "required": true 11 | } 12 | }, 13 | "stack": "container" 14 | } 15 | -------------------------------------------------------------------------------- /BasicRedisChat/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BasicRedisChat/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "RedisSettings": { 11 | "Url": "127.0.0.1", 12 | "Port": "6379", 13 | "Password": "123456" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BasicRedisChat/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | .eslintcache -------------------------------------------------------------------------------- /BasicRedisChat/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | 5 | ``` 6 | yarn install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | yarn start 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | yarn build 19 | ``` 20 | -------------------------------------------------------------------------------- /BasicRedisChat/client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.68a2faa8.chunk.css", 4 | "main.js": "/static/js/main.040969fe.chunk.js", 5 | "main.js.map": "/static/js/main.040969fe.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.c012fedc.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.c012fedc.js.map", 8 | "static/css/2.150d169a.chunk.css": "/static/css/2.150d169a.chunk.css", 9 | "static/js/2.6efd31e3.chunk.js": "/static/js/2.6efd31e3.chunk.js", 10 | "static/js/2.6efd31e3.chunk.js.map": "/static/js/2.6efd31e3.chunk.js.map", 11 | "index.html": "/index.html", 12 | "static/css/2.150d169a.chunk.css.map": "/static/css/2.150d169a.chunk.css.map", 13 | "static/css/main.68a2faa8.chunk.css.map": "/static/css/main.68a2faa8.chunk.css.map", 14 | "static/js/2.6efd31e3.chunk.js.LICENSE.txt": "/static/js/2.6efd31e3.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.c012fedc.js", 18 | "static/css/2.150d169a.chunk.css", 19 | "static/js/2.6efd31e3.chunk.js", 20 | "static/css/main.68a2faa8.chunk.css", 21 | "static/js/main.040969fe.chunk.js" 22 | ] 23 | } -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/0.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/1.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/10.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/11.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/12.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/2.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/3.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/4.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/5.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/6.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/7.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/8.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/avatars/9.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/build/favicon.ico -------------------------------------------------------------------------------- /BasicRedisChat/client/build/index.html: -------------------------------------------------------------------------------- 1 | ASP.NET Core Redis chat
-------------------------------------------------------------------------------- /BasicRedisChat/client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /BasicRedisChat/client/build/static/css/main.68a2faa8.chunk.css: -------------------------------------------------------------------------------- 1 | :root{--primary:#556ee6!important;--light:#f5f5f8!important;--success:#34c38f!important}.bg-success{background-color:#34c38f!important;background-color:var(--success)!important}.bg-light{background-color:#f5f5f8!important;background-color:var(--light)!important}.bg-gray{background-color:var(--gray)!important}.bg-primary{background-color:#556ee6!important;background-color:var(--primary)!important}.text-primary{color:#556ee6!important;color:var(--primary)!important}.list-group-item.active{background-color:#556ee6!important;background-color:var(--primary)!important;border-color:#556ee6!important;border-color:var(--primary)!important}.btn-rounded{border-radius:30px!important}.btn{display:inline-block;font-weight:400;color:#495057;text-align:center;vertical-align:middle;-webkit-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;border-radius:30px!important;padding:.47rem .75rem;font-size:.8125rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn-primary{color:#fff;background-color:#556ee6;background-color:var(--primary);border-color:#556ee6;border-color:var(--primary)}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#3452e1;border-color:#2948df}.font-size-14{font-size:14px!important}.font-size-11{font-size:11px!important}.font-size-12{font-size:12px!important}.font-size-15{font-size:15px!important}.w-md{min-width:110px}body{font-family:"Poppins",Arial,Helvetica,sans-serif;font-size:13px;color:#495057}.navbar{box-shadow:0 12px 24px 0 rgba(18,38,63,.03)}.navbar-brand{font-size:16px}.chats-title{padding-left:14px}.login-page{display:flex;align-items:center;flex-direction:column;justify-content:center;padding-bottom:190px;height:100vh}.form-signin{width:100%;max-width:330px;padding:15px;margin:0 auto}.text-small{font-size:.9rem}.chat-box,.messages-box{width:100%}.chat-box-wrapper{flex:1 1;overflow-y:scroll}.rounded-lg{border-radius:.5rem}input::-webkit-input-placeholder{font-size:.9rem;color:#999}input:-ms-input-placeholder{font-size:.9rem;color:#999}input::placeholder{font-size:.9rem;color:#999}.centered-box{width:100%;height:100vh;display:flex;align-items:center;justify-content:center}.login-error-anchor{position:relative}.toast-box{text-align:left;margin-top:30px;position:absolute;width:100%;top:0;display:flex;flex-direction:row;justify-content:center}.full-height{height:100vh;flex-direction:column;display:flex}.full-height .container{flex:1 1}.container .row{height:100%}.flex-column{display:flex;flex-direction:column}.bg-white.flex-column{height:100%}.flex{flex:1 1}.logout-button{cursor:pointer;display:flex;flex-direction:row;align-items:center;padding:15px 20px}.logout-button svg{margin-right:15px}.no-messages{opacity:.5;height:100%;width:100%}.avatar-box{width:50px;height:50px;object-fit:cover;object-position:50%;overflow:hidden;border-radius:4px}.avatar-box,.user-link{cursor:pointer}.user-link:hover{text-decoration:underline}.online-indicator{width:14px;height:14px;border:2px solid #fff;bottom:-7px;right:-7px;background-color:#4df573}.online-indicator.selected{border:none;width:12px;height:12px;bottom:-5px;right:-5px}.online-indicator.offline{background-color:#bbb}span.pseudo-link{font-size:14px;text-decoration:underline;color:var(--blue);cursor:pointer}span.pseudo-link:hover{text-decoration:none}.list-group-item{cursor:pointer;height:70px;box-sizing:border-box;transition:background-color .1s ease-out}.chat-icon{width:45px;height:45px;border-radius:4px;background-color:#eee}.chat-icon.active{background-color:var(--blue)}.chats-title{font-size:15px}.chat-body{border-radius:10px!important}.chat-list-container{height:100%}.chat-input{border-radius:30px!important;background-color:#eff2f7!important;border-color:#eff2f7!important;padding-right:120px}.form-control::-webkit-input-placeholder{font-size:13px}.form-control:-ms-input-placeholder{font-size:13px}.form-control::placeholder{font-size:13px}.form-control{display:block;width:100%;height:calc(1.5em + .94rem + 2px);padding:7.5px 12px;font-size:13px;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.rounded-button{border-radius:30px;background-color:var(--light)}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}.login-form .username-select button{background-color:transparent!important;color:inherit;padding:7.5px 12px!important;display:block!important;border:1px solid #ced4da!important;border-radius:4px!important;width:100%!important;text-align:left!important}.login-form .username-select .btn-primary:not(:disabled):not(.disabled).active,.login-form .username-select .btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:inherit}.login-form .username-select .dropdown-menu.show.dropdown-menu-right{transform:translateY(38px)!important}.username-select-dropdown{position:relative;display:flex!important;align-items:center;background-color:transparent!important;color:inherit;padding:0 12px!important;border:1px solid #ced4da!important;border-radius:4px!important;width:100%!important;text-align:left!important;height:calc(1.5em + .94rem + 2px)!important;cursor:pointer}.username-select-dropdown .username-select-block{background-color:var(--white);position:absolute;top:-1138px;left:0;opacity:0;transform:scale(.5);transform-origin:top left;transition:opacity .2s ease,transform .2s ease;border:1px solid #ced4da!important;border-radius:4px!important;padding:8px 0}.username-select-dropdown:focus{outline:none;border-color:#80bdff!important;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.username-select-dropdown .username-select-block.open{top:42px;transform:scale(1);opacity:1}.username-select-row{display:flex;width:100%;justify-content:space-between;align-items:center}.username-select-dropdown .username-select-block .username-select-block-item{padding:4px 24px}.username-select-dropdown .username-select-block .username-select-block-item:hover{background-color:var(--light)}.chat-list-item{cursor:pointer;padding:14px 16px}.mdi-circle:before{content:"󰝥"}.mdi-set,.mdi:before{display:inline-block;font:normal normal normal 24px/1 Material Design Icons;font-size:inherit;text-rendering:auto;line-height:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} 2 | /*# sourceMappingURL=main.68a2faa8.chunk.css.map */ -------------------------------------------------------------------------------- /BasicRedisChat/client/build/static/css/main.68a2faa8.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://src/styles/style-overrides.css","webpack://src/styles/style.css","webpack://src/styles/font-face.css","webpack://src/components/Login/style.css","webpack://src/components/Chat/components/ChatList/components/ChatListItem/style.css"],"names":[],"mappings":"AAAA,MACE,2BAAuC,CACvC,yBAA2B,CAC3B,2BACF,CAEA,YACE,kCAA2C,CAA3C,yCACF,CAEA,UACE,kCAAyC,CAAzC,uCACF,CAEA,SACE,sCACF,CAEA,YACE,kCAA2C,CAA3C,yCACF,CAEA,cACE,uBAAgC,CAAhC,8BACF,CAEA,wBACE,kCAA2C,CAA3C,yCAA2C,CAC3C,8BAAuC,CAAvC,qCACF,CAEA,aACE,4BACF,CAEA,KACE,oBAAqB,CACrB,eAAgB,CAChB,aAAc,CACd,iBAAkB,CAClB,qBAAsB,CACtB,wBAAiB,CAAjB,oBAAiB,CAAjB,gBAAiB,CACjB,4BAA6B,CAC7B,4BAA6B,CAC7B,4BAA8B,CAC9B,qBAAwB,CACxB,kBAAoB,CACpB,eAAgB,CAChB,oBAAsB,CAGtB,6HAKF,CAEA,aACE,UAAW,CACX,wBAAgC,CAAhC,+BAAgC,CAChC,oBAA4B,CAA5B,2BACF,CAEA,yDAGE,UAAW,CACX,wBAAyB,CACzB,oBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,MACE,eACF,CC1FA,KACE,gDAAoD,CACpD,cAAe,CACf,aACF,CAEA,QACE,2CACF,CAEA,cACE,cACF,CAEA,aACE,iBACF,CAEA,YACE,YAAa,CACb,kBAAmB,CACnB,qBAAsB,CACtB,sBAAuB,CACvB,oBAAqB,CACrB,YACF,CAEA,aACE,UAAW,CACX,eAAgB,CAChB,YAAa,CACb,aACF,CAEA,YACE,eACF,CAEA,wBAGE,UACF,CAEA,kBACE,QAAO,CACP,iBACF,CAEA,YACE,mBACF,CAEA,iCACE,eAAiB,CACjB,UACF,CAHA,4BACE,eAAiB,CACjB,UACF,CAHA,mBACE,eAAiB,CACjB,UACF,CAEA,cACE,UAAW,CACX,YAAa,CACb,YAAa,CACb,kBAAmB,CACnB,sBACF,CAEA,oBACE,iBACF,CAEA,WACE,eAAgB,CAChB,eAAgB,CAChB,iBAAkB,CAClB,UAAW,CACX,KAAM,CACN,YAAa,CACb,kBAAmB,CACnB,sBACF,CAEA,aACE,YAAa,CACb,qBAAsB,CACtB,YACF,CAEA,wBACE,QACF,CAEA,gBACE,WACF,CAEA,aACE,YAAa,CACb,qBACF,CAEA,sBACE,WACF,CAEA,MACE,QACF,CAEA,eACE,cAAe,CACf,YAAa,CACb,kBAAmB,CACnB,kBAAmB,CACnB,iBACF,CAEA,mBACE,iBACF,CAEA,aACE,UAAY,CACZ,WAAY,CACZ,UACF,CAEA,YACE,UAAW,CACX,WAAY,CACZ,gBAAiB,CACjB,mBAAoB,CACpB,eAAgB,CAChB,iBAEF,CAEA,uBAHE,cAKF,CAEA,iBACE,yBACF,CAEA,kBACE,UAAW,CACX,WAAY,CACZ,qBAAuB,CACvB,WAAY,CACZ,UAAW,CACX,wBACF,CAEA,2BACE,WAAY,CACZ,UAAW,CACX,WAAY,CACZ,WAAY,CACZ,UACF,CAEA,0BACE,qBACF,CAEA,iBACE,cAAe,CACf,yBAA0B,CAC1B,iBAAkB,CAClB,cACF,CAEA,uBACE,oBACF,CAEA,iBACE,cAAe,CACf,WAAY,CACZ,qBAAsB,CACtB,wCACF,CAEA,WACE,UAAW,CACX,WAAY,CACZ,iBAAkB,CAClB,qBACF,CAEA,kBACE,4BACF,CAEA,aACE,cACF,CAEA,WACE,4BACF,CAEA,qBACE,WACF,CAEA,YACE,4BAA8B,CAC9B,kCAAoC,CACpC,8BAAgC,CAChC,mBACF,CAEA,yCACE,cACF,CAFA,oCACE,cACF,CAFA,2BACE,cACF,CAEA,cACE,aAAc,CACd,UAAW,CACX,iCAAmC,CACnC,kBAAmB,CACnB,cAAe,CACf,eAAgB,CAChB,eAAgB,CAChB,aAAc,CACd,qBAAsB,CACtB,2BAA4B,CAC5B,wBAAyB,CAKzB,oEAGF,CAEA,gBACE,kBAAmB,CACnB,6BACF,CChPA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,qGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,qGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,kGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CCzKA,oCACE,sCAAwC,CACxC,aAAc,CACd,4BAA8B,CAC9B,uBAAyB,CACzB,kCAA+C,CAC/C,2BAA6B,CAC7B,oBAAsB,CACtB,yBACF,CAEA,iMAGE,aACF,CAEA,qEACE,oCACF,CAEA,0BACE,iBAAkB,CAClB,sBAAwB,CACxB,kBAAmB,CACnB,sCAAwC,CACxC,aAAc,CACd,wBAA0B,CAC1B,kCAA+C,CAC/C,2BAA6B,CAC7B,oBAAsB,CACtB,yBAA2B,CAC3B,2CAA8C,CAE9C,cACF,CAEA,iDACE,6BAA8B,CAC9B,iBAAkB,CAClB,WAAY,CACZ,MAAO,CACP,SAAU,CACV,mBAA0B,CAC1B,yBAA0B,CAC1B,8CAAkD,CAElD,kCAA+C,CAC/C,2BAA6B,CAE7B,aACF,CAEA,gCACE,YAAa,CACb,8BAAgC,CAChC,0CACF,CAEA,sDACE,QAAS,CACT,kBAAsB,CACtB,SACF,CAEA,qBACE,YAAa,CACb,UAAW,CACX,6BAA8B,CAC9B,kBACF,CAEA,6EACE,gBACF,CAEA,mFAGE,6BACF,CChFA,gBACE,cAAe,CACf,iBACF,CACA,mBACE,YACF,CAEA,qBAEE,oBAAqB,CACrB,sDAAuD,CACvD,iBAAkB,CAClB,mBAAoB,CACpB,mBAAoB,CACpB,kCAAmC,CACnC,iCACF","file":"main.68a2faa8.chunk.css","sourcesContent":[":root {\r\n --primary: rgb(85, 110, 230) !important;\r\n --light: #f5f5f8 !important;\r\n --success: rgb(52, 195, 143) !important;\r\n}\r\n\r\n.bg-success {\r\n background-color: var(--success) !important;\r\n}\r\n\r\n.bg-light {\r\n background-color: var(--light) !important;\r\n}\r\n\r\n.bg-gray {\r\n background-color: var(--gray) !important;\r\n}\r\n\r\n.bg-primary {\r\n background-color: var(--primary) !important;\r\n}\r\n\r\n.text-primary {\r\n color: var(--primary) !important;\r\n}\r\n\r\n.list-group-item.active {\r\n background-color: var(--primary) !important;\r\n border-color: var(--primary) !important;\r\n}\r\n\r\n.btn-rounded {\r\n border-radius: 30px !important;\r\n}\r\n\r\n.btn {\r\n display: inline-block;\r\n font-weight: 400;\r\n color: #495057;\r\n text-align: center;\r\n vertical-align: middle;\r\n user-select: none;\r\n background-color: transparent;\r\n border: 1px solid transparent;\r\n border-radius: 30px !important;\r\n padding: 0.47rem 0.75rem;\r\n font-size: 0.8125rem;\r\n line-height: 1.5;\r\n border-radius: 0.25rem;\r\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\r\n border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;\r\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\r\n border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\r\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\r\n border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n}\r\n\r\n.btn-primary {\r\n color: #fff;\r\n background-color: var(--primary);\r\n border-color: var(--primary);\r\n}\r\n\r\n.btn-primary.focus,\r\n.btn-primary:focus,\r\n.btn-primary:hover {\r\n color: #fff;\r\n background-color: #3452e1;\r\n border-color: #2948df;\r\n}\r\n\r\n.font-size-14 {\r\n font-size: 14px !important;\r\n}\r\n\r\n.font-size-11 {\r\n font-size: 11px !important;\r\n}\r\n\r\n.font-size-12 {\r\n font-size: 12px !important;\r\n}\r\n\r\n.font-size-15 {\r\n font-size: 15px !important;\r\n}\r\n\r\n.w-md {\r\n min-width: 110px;\r\n}\r\n","body {\r\n font-family: \"Poppins\", Arial, Helvetica, sans-serif;\r\n font-size: 13px;\r\n color: #495057;\r\n}\r\n\r\n.navbar {\r\n box-shadow: rgba(18, 38, 63, 0.03) 0px 12px 24px 0px;\r\n}\r\n\r\n.navbar-brand {\r\n font-size: 16px;\r\n}\r\n\r\n.chats-title {\r\n padding-left: 14px;\r\n}\r\n\r\n.login-page {\r\n display: flex;\r\n align-items: center;\r\n flex-direction: column;\r\n justify-content: center;\r\n padding-bottom: 190px;\r\n height: 100vh;\r\n}\r\n\r\n.form-signin {\r\n width: 100%;\r\n max-width: 330px;\r\n padding: 15px;\r\n margin: 0 auto;\r\n}\r\n\r\n.text-small {\r\n font-size: 0.9rem;\r\n}\r\n\r\n.messages-box,\r\n.chat-box {\r\n /* height: 510px; */\r\n width: 100%;\r\n}\r\n\r\n.chat-box-wrapper {\r\n flex: 1;\r\n overflow-y: scroll;\r\n}\r\n\r\n.rounded-lg {\r\n border-radius: 0.5rem;\r\n}\r\n\r\ninput::placeholder {\r\n font-size: 0.9rem;\r\n color: #999;\r\n}\r\n\r\n.centered-box {\r\n width: 100%;\r\n height: 100vh;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.login-error-anchor {\r\n position: relative;\r\n}\r\n\r\n.toast-box {\r\n text-align: left;\r\n margin-top: 30px;\r\n position: absolute;\r\n width: 100%;\r\n top: 0;\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: center;\r\n}\r\n\r\n.full-height {\r\n height: 100vh;\r\n flex-direction: column;\r\n display: flex;\r\n}\r\n\r\n.full-height .container {\r\n flex: 1;\r\n}\r\n\r\n.container .row {\r\n height: 100%;\r\n}\r\n\r\n.flex-column {\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.bg-white.flex-column {\r\n height: 100%;\r\n}\r\n\r\n.flex {\r\n flex: 1;\r\n}\r\n\r\n.logout-button {\r\n cursor: pointer;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n padding: 15px 20px;\r\n}\r\n\r\n.logout-button svg {\r\n margin-right: 15px;\r\n}\r\n\r\n.no-messages {\r\n opacity: 0.5;\r\n height: 100%;\r\n width: 100%;\r\n}\r\n\r\n.avatar-box {\r\n width: 50px;\r\n height: 50px;\r\n object-fit: cover;\r\n object-position: 50%;\r\n overflow: hidden;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n}\r\n\r\n.user-link {\r\n cursor: pointer;\r\n}\r\n\r\n.user-link:hover {\r\n text-decoration: underline;\r\n}\r\n\r\n.online-indicator {\r\n width: 14px;\r\n height: 14px;\r\n border: 2px solid white;\r\n bottom: -7px;\r\n right: -7px;\r\n background-color: #4df573;\r\n}\r\n\r\n.online-indicator.selected {\r\n border: none;\r\n width: 12px;\r\n height: 12px;\r\n bottom: -5px;\r\n right: -5px;\r\n}\r\n\r\n.online-indicator.offline {\r\n background-color: #bbb;\r\n}\r\n\r\nspan.pseudo-link {\r\n font-size: 14px;\r\n text-decoration: underline;\r\n color: var(--blue);\r\n cursor: pointer;\r\n}\r\n\r\nspan.pseudo-link:hover {\r\n text-decoration: none;\r\n}\r\n\r\n.list-group-item {\r\n cursor: pointer;\r\n height: 70px;\r\n box-sizing: border-box;\r\n transition: background-color 0.1s ease-out;\r\n}\r\n\r\n.chat-icon {\r\n width: 45px;\r\n height: 45px;\r\n border-radius: 4px;\r\n background-color: #eee;\r\n}\r\n\r\n.chat-icon.active {\r\n background-color: var(--blue);\r\n}\r\n\r\n.chats-title {\r\n font-size: 15px;\r\n}\r\n\r\n.chat-body {\r\n border-radius: 10px !important;\r\n}\r\n\r\n.chat-list-container {\r\n height: 100%;\r\n}\r\n\r\n.chat-input {\r\n border-radius: 30px !important;\r\n background-color: #eff2f7 !important;\r\n border-color: #eff2f7 !important;\r\n padding-right: 120px;\r\n}\r\n\r\n.form-control::placeholder {\r\n font-size: 13px;\r\n}\r\n\r\n.form-control {\r\n display: block;\r\n width: 100%;\r\n height: calc(1.5em + 0.94rem + 2px);\r\n padding: 7.5px 12px;\r\n font-size: 13px;\r\n font-weight: 400;\r\n line-height: 1.5;\r\n color: #495057;\r\n background-color: #fff;\r\n background-clip: padding-box;\r\n border: 1px solid #ced4da;\r\n -webkit-transition: border-color 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n}\r\n\r\n.rounded-button {\r\n border-radius: 30px;\r\n background-color: var(--light);\r\n}\r\n","/* devanagari */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 300;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\r\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\r\n}\r\n/* latin-ext */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 300;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\r\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n}\r\n/* latin */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 300;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\r\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\r\n U+FEFF, U+FFFD;\r\n}\r\n/* devanagari */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 400;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\r\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\r\n}\r\n/* latin-ext */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 400;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\r\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n}\r\n/* latin */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 400;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\r\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\r\n U+FEFF, U+FFFD;\r\n}\r\n/* devanagari */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 500;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\r\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\r\n}\r\n/* latin-ext */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 500;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\r\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n}\r\n/* latin */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 500;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\r\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\r\n U+FEFF, U+FFFD;\r\n}\r\n/* devanagari */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 600;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\r\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\r\n}\r\n/* latin-ext */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 600;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\r\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n}\r\n/* latin */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 600;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\r\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\r\n U+FEFF, U+FFFD;\r\n}\r\n/* devanagari */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 700;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\r\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\r\n}\r\n/* latin-ext */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 700;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\r\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\r\n}\r\n/* latin */\r\n@font-face {\r\n font-family: \"Poppins\";\r\n font-style: normal;\r\n font-weight: 700;\r\n font-display: swap;\r\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2)\r\n format(\"woff2\");\r\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\r\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\r\n U+FEFF, U+FFFD;\r\n}\r\n",".login-form .username-select button {\r\n background-color: transparent !important;\r\n color: inherit;\r\n padding: 7.5px 12px !important;\r\n display: block !important;\r\n border: 1px solid rgb(206, 212, 218) !important;\r\n border-radius: 4px !important;\r\n width: 100% !important;\r\n text-align: left !important;\r\n}\r\n\r\n.login-form .username-select .btn-primary:not(:disabled):not(.disabled).active,\r\n.login-form .username-select .btn-primary:not(:disabled):not(.disabled):active,\r\n.show > .btn-primary.dropdown-toggle {\r\n color: inherit;\r\n}\r\n\r\n.login-form .username-select .dropdown-menu.show.dropdown-menu-right {\r\n transform: translate(0px, 38px) !important;\r\n}\r\n\r\n.username-select-dropdown {\r\n position: relative;\r\n display: flex !important;\r\n align-items: center;\r\n background-color: transparent !important;\r\n color: inherit;\r\n padding: 0 12px !important;\r\n border: 1px solid rgb(206, 212, 218) !important;\r\n border-radius: 4px !important;\r\n width: 100% !important;\r\n text-align: left !important;\r\n height: calc(1.5em + 0.94rem + 2px) !important;\r\n\r\n cursor: pointer;\r\n}\r\n\r\n.username-select-dropdown .username-select-block {\r\n background-color: var(--white);\r\n position: absolute;\r\n top: -1138px;\r\n left: 0;\r\n opacity: 0;\r\n transform: scale(0.5, 0.5);\r\n transform-origin: top left;\r\n transition: opacity 0.2s ease, transform 0.2s ease;\r\n\r\n border: 1px solid rgb(206, 212, 218) !important;\r\n border-radius: 4px !important;\r\n\r\n padding: 8px 0px;\r\n}\r\n\r\n.username-select-dropdown:focus {\r\n outline: none;\r\n border-color: #80bdff !important;\r\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\r\n}\r\n\r\n.username-select-dropdown .username-select-block.open {\r\n top: 42px;\r\n transform: scale(1, 1);\r\n opacity: 1;\r\n}\r\n\r\n.username-select-row {\r\n display: flex;\r\n width: 100%;\r\n justify-content: space-between;\r\n align-items: center;\r\n}\r\n\r\n.username-select-dropdown .username-select-block .username-select-block-item {\r\n padding: 4px 24px;\r\n}\r\n\r\n.username-select-dropdown\r\n .username-select-block\r\n .username-select-block-item:hover {\r\n background-color: var(--light);\r\n}\r\n",".chat-list-item {\r\n cursor: pointer;\r\n padding: 14px 16px;\r\n}\r\n.mdi-circle:before {\r\n content: \"󰝥\";\r\n}\r\n\r\n.mdi-set,\r\n.mdi:before {\r\n display: inline-block;\r\n font: normal normal normal 24px/1 Material Design Icons;\r\n font-size: inherit;\r\n text-rendering: auto;\r\n line-height: inherit;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n}\r\n"]} -------------------------------------------------------------------------------- /BasicRedisChat/client/build/static/js/2.6efd31e3.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | * The buffer module from node.js, for the browser. 15 | * 16 | * @author Feross Aboukhadijeh 17 | * @license MIT 18 | */ 19 | 20 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 21 | 22 | /** @license React v0.20.1 23 | * scheduler.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.1 32 | * react-dom.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.1 41 | * react-jsx-runtime.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | /** @license React v17.0.1 50 | * react.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | 58 | //! moment.js 59 | -------------------------------------------------------------------------------- /BasicRedisChat/client/build/static/js/runtime-main.c012fedc.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,l,i=t[0],f=t[1],a=t[2],p=0,s=[];p0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@types/socket.io-client": "^1.4.34" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/0.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/1.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/10.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/11.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/12.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/2.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/3.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/4.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/5.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/6.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/7.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/8.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/avatars/9.jpg -------------------------------------------------------------------------------- /BasicRedisChat/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/favicon.ico -------------------------------------------------------------------------------- /BasicRedisChat/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | ASP.NET Core Redis chat 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /BasicRedisChat/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /BasicRedisChat/client/public/welcome-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/BasicRedisChat/client/public/welcome-back.png -------------------------------------------------------------------------------- /BasicRedisChat/client/src/App.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useEffect, useCallback } from "react"; 3 | import Login from "./components/Login"; 4 | import Chat from "./components/Chat"; 5 | import { getOnlineUsers, getRooms } from "./api"; 6 | import useAppStateContext, { AppContext } from "./state"; 7 | import moment from "moment"; 8 | import { parseRoomName } from "./utils"; 9 | import { LoadingScreen } from "./components/LoadingScreen"; 10 | import Navbar from "./components/Navbar"; 11 | import { useUser } from "./hooks"; 12 | import { useSocket } from "./use-socket"; 13 | 14 | const App = () => { 15 | const { 16 | loading, 17 | user, 18 | state, 19 | dispatch, 20 | onLogIn, 21 | onMessageSend, 22 | onLogOut, 23 | } = useAppHandlers(); 24 | 25 | if (loading) { 26 | return ; 27 | } 28 | 29 | const showLogin = !user; 30 | 31 | return ( 32 | 33 |
39 | 40 | {showLogin ? ( 41 | 42 | ) : ( 43 | 44 | )} 45 |
46 |
47 | ); 48 | }; 49 | 50 | const useAppHandlers = () => { 51 | const [state, dispatch] = useAppStateContext(); 52 | const onUserLoaded = useCallback( 53 | (user) => { 54 | if (user !== null) { 55 | if (!state.users[user.id]) { 56 | dispatch({ type: "set user", payload: { ...user, online: true } }); 57 | } 58 | } 59 | }, 60 | [dispatch, state.users] 61 | ); 62 | 63 | const { user, onLogIn, onLogOut: onLogOutA, loading } = useUser( 64 | onUserLoaded, 65 | dispatch 66 | ); 67 | const [socket, connected, onLogOut] = useSocket(user, dispatch, onLogOutA); 68 | 69 | /** Socket joins specific rooms once they are added */ 70 | useEffect(() => { 71 | if (user === null) { 72 | /** We are logged out */ 73 | /** But it's necessary to pre-populate the main room, so the user won't wait for messages once he's logged in */ 74 | return; 75 | } 76 | if (connected) { 77 | /** 78 | * The socket needs to be joined to the newly added rooms 79 | * on an active connection. 80 | */ 81 | const newRooms = []; 82 | Object.keys(state.rooms).forEach((roomId) => { 83 | const room = state.rooms[roomId]; 84 | if (room.connected) { 85 | return; 86 | } 87 | newRooms.push({ ...room, connected: true }); 88 | }); 89 | if (newRooms.length !== 0) { 90 | dispatch({ type: "set rooms", payload: newRooms }); 91 | } 92 | } else { 93 | /** 94 | * It's necessary to set disconnected flags on rooms 95 | * once the client is not connected 96 | */ 97 | const newRooms = []; 98 | Object.keys(state.rooms).forEach((roomId) => { 99 | const room = state.rooms[roomId]; 100 | if (!room.connected) { 101 | return; 102 | } 103 | newRooms.push({ ...room, connected: false }); 104 | }); 105 | /** Only update the state if it's only necessary */ 106 | if (newRooms.length !== 0) { 107 | dispatch({ type: "set rooms", payload: newRooms }); 108 | } 109 | } 110 | }, [user, connected, dispatch, socket, state.rooms, state.users]); 111 | 112 | /** Populate default rooms when user is not null */ 113 | useEffect(() => { 114 | /** @ts-ignore */ 115 | if (Object.values(state.rooms).length === 0 && user !== null) { 116 | /** First of all fetch online users. */ 117 | getOnlineUsers().then((users) => { 118 | dispatch({ 119 | type: "append users", 120 | payload: users, 121 | }); 122 | }); 123 | /** Then get rooms. */ 124 | getRooms(user.id).then((rooms) => { 125 | const payload = []; 126 | rooms.forEach(({ id, names }) => { 127 | payload.push({ id, name: parseRoomName(names, user.username) }); 128 | }); 129 | /** Here we also can populate the state with default chat rooms */ 130 | dispatch({ 131 | type: "set rooms", 132 | payload, 133 | }); 134 | dispatch({ type: "set current room", payload: "0" }); 135 | }); 136 | } 137 | }, [dispatch, state.rooms, user]); 138 | 139 | const onMessageSend = useCallback( 140 | (message, roomId) => { 141 | if (typeof message !== "string" || message.trim().length === 0) { 142 | return; 143 | } 144 | if (!socket) { 145 | /** Normally there shouldn't be such case. */ 146 | console.error("Couldn't send message"); 147 | } 148 | socket.emit("message", { 149 | roomId: roomId, 150 | message, 151 | from: user.id, 152 | date: moment(new Date()).unix(), 153 | }); 154 | }, 155 | [user, socket] 156 | ); 157 | 158 | return { 159 | loading, 160 | user, 161 | state, 162 | dispatch, 163 | onLogIn, 164 | onMessageSend, 165 | onLogOut, 166 | }; 167 | }; 168 | 169 | export default App; 170 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | axios.defaults.withCredentials = true; 3 | 4 | const BASE_URL = ''; 5 | 6 | export const MESSAGES_TO_LOAD = 15; 7 | 8 | const url = x => `${BASE_URL}${x}`; 9 | 10 | /** Checks if there's an existing session. */ 11 | export const getMe = () => { 12 | return axios.get(url('/users/me')) 13 | .then(x => x.data) 14 | .catch(_ => null); 15 | }; 16 | 17 | /** 18 | * Fetch users by requested ids 19 | * @param {Array} ids 20 | */ 21 | export const getUsers = (ids) => { 22 | return axios.get(url(`/users`), { params: { ids : ids || [] } }).then(x => x.data); 23 | }; 24 | 25 | /** Fetch users which are online */ 26 | export const getOnlineUsers = () => { 27 | return axios.get(url(`/users/online`)).then(x => x.data); 28 | }; 29 | 30 | /** Handle user log in */ 31 | export const login = (username, password) => { 32 | return axios.post(url('/auth/login'), { 33 | username, 34 | password 35 | }).then(x => 36 | x.data 37 | ) 38 | .catch(e => { throw new Error(e.response && e.response.data && e.response.data.message); }); 39 | }; 40 | 41 | export const logOut = () => { 42 | return axios.post(url('/auth/logout')); 43 | }; 44 | 45 | /** 46 | * Function for checking which deployment urls exist. 47 | * 48 | * @returns {Promise<{ 49 | * heroku?: string; 50 | * google_cloud?: string; 51 | * vercel?: string; 52 | * github?: string; 53 | * }>} 54 | */ 55 | export const getButtonLinks = () => { 56 | return axios.get(url('/links')) 57 | .then(x => x.data) 58 | .catch(_ => null); 59 | }; 60 | 61 | /** 62 | * @returns {Promise>} 63 | */ 64 | export const getRooms = async (userId) => { 65 | return axios.get(url(`/rooms/user/${userId}`)).then(x => x.data); 66 | }; 67 | 68 | /** 69 | * Load messages 70 | * 71 | * @param {string} id room id 72 | * @param {number} offset 73 | * @param {number} size 74 | */ 75 | export const getMessages = (id, 76 | offset = 0, 77 | size = MESSAGES_TO_LOAD 78 | ) => { 79 | return axios.get(url(`/rooms/messages/${id}`), { 80 | params: { 81 | offset, 82 | size 83 | } 84 | }) 85 | .then(x => x.data.reverse()); 86 | }; 87 | 88 | /** This one is called on a private messages room created. */ 89 | export const addRoom = async (user1, user2) => { 90 | return axios.post(url(`/room`), { user1, user2 }).then(x => x.data); 91 | }; 92 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/ChatList/components/AvatarImage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useMemo } from "react"; 3 | import { getAvatarByUserAndRoomId } from "../../../../../utils"; 4 | import ChatIcon from "./ChatIcon"; 5 | 6 | const AvatarImage = ({ name, id }) => { 7 | const url = useMemo(() => { 8 | const av = getAvatarByUserAndRoomId("" + id); 9 | if (name === "Mary") { 10 | return `${process.env.PUBLIC_URL}/avatars/0.jpg`; 11 | } else if (name === "Pablo") { 12 | return `${process.env.PUBLIC_URL}/avatars/2.jpg`; 13 | } else if (name === "Joe") { 14 | return `${process.env.PUBLIC_URL}/avatars/9.jpg`; 15 | } else if (name === "Alex") { 16 | return `${process.env.PUBLIC_URL}/avatars/8.jpg`; 17 | } 18 | return av; 19 | }, [id, name]); 20 | 21 | return ( 22 | <> 23 | {name !== "General" ? ( 24 | {name} 30 | ) : ( 31 |
32 | 33 |
34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default AvatarImage; 40 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/ChatList/components/ChatIcon.jsx: -------------------------------------------------------------------------------- 1 | const ChatIcon = () => ( 2 | 9 | 10 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | export default ChatIcon; 33 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/ChatList/components/ChatListItem/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import "./style.css"; 3 | import React, { useMemo } from "react"; 4 | import { useAppState } from "../../../../../../state"; 5 | import moment from "moment"; 6 | import { useEffect } from "react"; 7 | import { getMessages } from "../../../../../../api"; 8 | import AvatarImage from "../AvatarImage"; 9 | import OnlineIndicator from "../../../OnlineIndicator"; 10 | 11 | /** 12 | * @param {{ active: boolean; room: import('../../../../../../state').Room; onClick: () => void; }} props 13 | */ 14 | const ChatListItem = ({ room, active = false, onClick }) => { 15 | const { online, name, lastMessage, userId } = useChatListItemHandlers(room); 16 | return ( 17 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
{name}
31 | {lastMessage && ( 32 |

{lastMessage.message}

33 | )} 34 |
35 | {lastMessage && ( 36 |
37 | {moment.unix(lastMessage.date).format("LT")} 38 |
39 | )} 40 |
41 | ); 42 | }; 43 | 44 | const useChatListItemHandlers = ( 45 | /** @type {import("../../../../../../state").Room} */ room 46 | ) => { 47 | const { id, name } = room; 48 | const [state] = useAppState(); 49 | 50 | /** Here we want to associate the room with a user by its name (since it's unique). */ 51 | const [isUser, online, userId] = useMemo(() => { 52 | try { 53 | let pseudoUserId = Math.abs(parseInt(id.split(":").reverse().pop())); 54 | const isUser = pseudoUserId > 0; 55 | const usersFiltered = Object.entries(state.users) 56 | .filter(([, user]) => user.username === name) 57 | .map(([, user]) => user); 58 | let online = false; 59 | if (usersFiltered.length > 0) { 60 | online = usersFiltered[0].online; 61 | pseudoUserId = +usersFiltered[0].id; 62 | } 63 | return [isUser, online, pseudoUserId]; 64 | } catch (_) { 65 | return [false, false, "0"]; 66 | } 67 | }, [id, name, state.users]); 68 | 69 | const lastMessage = useLastMessage(room); 70 | 71 | return { 72 | isUser, 73 | online, 74 | userId, 75 | name: room.name, 76 | lastMessage, 77 | }; 78 | }; 79 | 80 | const useLastMessage = ( 81 | /** @type {import("../../../../../../state").Room} */ room 82 | ) => { 83 | const [, dispatch] = useAppState(); 84 | const { lastMessage } = room; 85 | useEffect(() => { 86 | if (lastMessage === undefined) { 87 | /** need to fetch it */ 88 | if (room.messages === undefined) { 89 | getMessages(room.id, 0, 1).then((messages) => { 90 | let message = null; 91 | if (messages.length !== 0) { 92 | message = messages.pop(); 93 | } 94 | dispatch({ 95 | type: "set last message", 96 | payload: { id: room.id, lastMessage: message }, 97 | }); 98 | }); 99 | } else if (room.messages.length === 0) { 100 | dispatch({ 101 | type: "set last message", 102 | payload: { id: room.id, lastMessage: null }, 103 | }); 104 | } else { 105 | dispatch({ 106 | type: "set last message", 107 | payload: { 108 | id: room.id, 109 | lastMessage: room.messages[room.messages.length - 1], 110 | }, 111 | }); 112 | } 113 | } 114 | }, [lastMessage, dispatch, room]); 115 | 116 | return lastMessage; 117 | }; 118 | 119 | export default ChatListItem; 120 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/ChatList/components/ChatListItem/style.css: -------------------------------------------------------------------------------- 1 | .chat-list-item { 2 | cursor: pointer; 3 | padding: 14px 16px; 4 | } 5 | .mdi-circle:before { 6 | content: "󰝥"; 7 | } 8 | 9 | .mdi-set, 10 | .mdi:before { 11 | display: inline-block; 12 | font: normal normal normal 24px/1 Material Design Icons; 13 | font-size: inherit; 14 | text-rendering: auto; 15 | line-height: inherit; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/ChatList/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | import { Power } from "react-bootstrap-icons"; 5 | import OnlineIndicator from "../../OnlineIndicator"; 6 | import AvatarImage from "./AvatarImage"; 7 | 8 | const Footer = ({ user, onLogOut }) => ( 9 |
13 | {true ? ( 14 | <> 15 | 16 | 17 | 18 | ) : ( 19 | <> 20 | 21 | 22 | 23 | )} 24 |
25 | ); 26 | 27 | const LogoutButton = ({ onLogOut, col = 5, noinfo = false }) => ( 28 |
33 | Log out 34 |
35 | ); 36 | 37 | const UserInfo = ({ user, col = 7, noinfo = false }) => ( 38 |
43 |
44 | 45 |
46 | {!noinfo && ( 47 |
48 |
{user.username}
49 |
50 | 51 |

Active

52 |
53 |
54 | )} 55 |
56 | ); 57 | 58 | export default Footer; 59 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/ChatList/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useMemo } from "react"; 3 | import ChatListItem from "./components/ChatListItem"; 4 | import Footer from "./components/Footer"; 5 | 6 | const ChatList = ({ rooms, dispatch, user, currentRoom, onLogOut }) => { 7 | const processedRooms = useMemo(() => { 8 | const roomsList = Object.values(rooms); 9 | const main = roomsList.filter((x) => x.id === "0"); 10 | let other = roomsList.filter((x) => x.id !== "0"); 11 | other = other.sort( 12 | (a, b) => +a.id.split(":").pop() - +b.id.split(":").pop() 13 | ); 14 | return [...(main ? main : []), ...other]; 15 | }, [rooms]); 16 | return ( 17 | <> 18 |
19 |
20 |

Chats

21 |
22 |
23 |
24 | {processedRooms.map((room) => ( 25 | 28 | dispatch({ type: "set current room", payload: room.id }) 29 | } 30 | active={currentRoom === room.id} 31 | room={room} 32 | /> 33 | ))} 34 |
35 |
36 |
37 |
38 | 39 | ); 40 | }; 41 | 42 | export default ChatList; 43 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/components/ClockIcon.jsx: -------------------------------------------------------------------------------- 1 | const ClockIcon = () => ( 2 | 12 | ); 13 | 14 | export default ClockIcon; 15 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/components/InfoMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const InfoMessage = ({ message }) => { 3 | return ( 4 |

8 | {message} 9 |

10 | ); 11 | }; 12 | 13 | export default InfoMessage; 14 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/components/MessagesLoading.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | 4 | const MessagesLoading = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default MessagesLoading; 15 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/components/NoMessages.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import { CardText } from "react-bootstrap-icons"; 4 | 5 | const NoMessages = () => { 6 | return ( 7 |
8 | 9 |

No messages

10 |
11 | ); 12 | }; 13 | 14 | export default NoMessages; 15 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/components/ReceiverMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import moment from "moment"; 3 | import React from "react"; 4 | import ClockIcon from "./ClockIcon"; 5 | 6 | const ReceiverMessage = ({ 7 | username = "user", 8 | message = "Lorem ipsum dolor...", 9 | date, 10 | }) => ( 11 |
12 |
13 |
14 |
18 |
19 |
25 | {username} 26 |
27 |

{message}

28 |

29 | {moment.unix(date).format("LT")}{" "} 30 |

31 |
32 |
33 |
34 |
35 | ); 36 | export default ReceiverMessage; 37 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/components/SenderMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import moment from "moment"; 3 | import React from "react"; 4 | import ClockIcon from "./ClockIcon"; 5 | import OnlineIndicator from "../../OnlineIndicator"; 6 | 7 | const SenderMessage = ({ 8 | user, 9 | message = "Lorem ipsum dolor...", 10 | date, 11 | onUserClicked, 12 | }) => ( 13 |
14 |
15 |
19 |
20 | {user && ( 21 |
22 |
30 | {user.username} 31 |
32 | 33 |
34 | )} 35 |

{message}

36 |

37 | {moment.unix(date).format("LT")}{" "} 38 |

39 |
40 |
41 |
42 |
43 |
44 | ); 45 | 46 | export default SenderMessage; 47 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/MessageList/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import { MESSAGES_TO_LOAD } from "../../../../api"; 4 | import InfoMessage from "./components/InfoMessage"; 5 | import MessagesLoading from "./components/MessagesLoading"; 6 | import NoMessages from "./components/NoMessages"; 7 | import ReceiverMessage from "./components/ReceiverMessage"; 8 | import SenderMessage from "./components/SenderMessage"; 9 | 10 | const MessageList = ({ 11 | messageListElement, 12 | messages, 13 | room, 14 | onLoadMoreMessages, 15 | user, 16 | onUserClicked, 17 | users, 18 | }) => ( 19 |
23 | {messages === undefined ? ( 24 | 25 | ) : messages.length === 0 ? ( 26 | 27 | ) : ( 28 | <> 29 | )} 30 |
31 | {messages && messages.length !== 0 && ( 32 | <> 33 | {room.offset && room.offset >= MESSAGES_TO_LOAD ? ( 34 |
35 |
38 |
39 | 49 |
50 |
53 |
54 | ) : ( 55 | <> 56 | )} 57 | {messages.map((message, x) => { 58 | const key = message.message + message.date + message.from + x; 59 | if (message.from === "info") { 60 | return ; 61 | } 62 | if (+message.from !== +user.id) { 63 | return ( 64 | onUserClicked(message.from)} 66 | key={key} 67 | message={message.message} 68 | date={message.date} 69 | user={users[message.from]} 70 | /> 71 | ); 72 | } 73 | return ( 74 | 82 | ); 83 | })} 84 | 85 | )} 86 |
87 |
88 | ); 89 | export default MessageList; 90 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/OnlineIndicator.jsx: -------------------------------------------------------------------------------- 1 | const OnlineIndicator = ({ online, hide = false, width = 8, height = 8 }) => { 2 | return ( 3 |
9 | ); 10 | }; 11 | 12 | export default OnlineIndicator; 13 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/components/TypingArea.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const TypingArea = ({ message, setMessage, onSubmit }) => ( 3 |
4 |
5 |
6 |
7 | setMessage(e.target.value)} 10 | type="text" 11 | placeholder="Enter Message..." 12 | className="form-control chat-input" 13 | /> 14 | {/**/} 15 |
16 |
17 |
18 | 27 |
28 |
29 |
30 | ); 31 | 32 | export default TypingArea; 33 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import ChatList from "./components/ChatList"; 4 | import MessageList from "./components/MessageList"; 5 | import TypingArea from "./components/TypingArea"; 6 | import useChatHandlers from "./use-chat-handlers"; 7 | 8 | /** 9 | * @param {{ 10 | * onLogOut: () => void, 11 | * onMessageSend: (message: string, roomId: string) => void, 12 | * user: import("../../state").UserEntry 13 | * }} props 14 | */ 15 | export default function Chat({ onLogOut, user, onMessageSend }) { 16 | const { 17 | onLoadMoreMessages, 18 | onUserClicked, 19 | message, 20 | setMessage, 21 | rooms, 22 | room, 23 | currentRoom, 24 | dispatch, 25 | messageListElement, 26 | roomId, 27 | messages, 28 | users, 29 | } = useChatHandlers(user); 30 | 31 | return ( 32 |
33 |
34 |
35 | 42 |
43 | {/* Chat Box*/} 44 |
45 |
46 |

{room ? room.name : "Room"}

47 |
48 | 57 | 58 | {/* Typing area */} 59 | { 63 | e.preventDefault(); 64 | onMessageSend(message.trim(), roomId); 65 | setMessage(""); 66 | 67 | messageListElement.current.scrollTop = 68 | messageListElement.current.scrollHeight; 69 | }} 70 | /> 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Chat/use-chat-handlers.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useCallback } from "react"; 3 | import { useEffect, useState, useRef } from "react"; 4 | import { addRoom, getMessages } from "../../api"; 5 | import { useAppState } from "../../state"; 6 | import { parseRoomName, populateUsersFromLoadedMessages } from "../../utils"; 7 | 8 | /** Lifecycle hooks with callbacks for the Chat component */ 9 | const useChatHandlers = (/** @type {import("../../state").UserEntry} */ user) => { 10 | const [state, dispatch] = useAppState(); 11 | const messageListElement = useRef(null); 12 | 13 | /** @type {import("../../state").Room} */ 14 | const room = state.rooms[state.currentRoom]; 15 | const roomId = room?.id; 16 | const messages = room?.messages; 17 | 18 | const [message, setMessage] = useState(""); 19 | 20 | const scrollToTop = useCallback(() => { 21 | setTimeout(() => { 22 | if (messageListElement.current) { 23 | messageListElement.current.scrollTop = 0; 24 | } 25 | }, 0); 26 | }, []); 27 | 28 | const scrollToBottom = useCallback(() => { 29 | if (messageListElement.current) { 30 | messageListElement.current.scrollTo({ 31 | top: messageListElement.current.scrollHeight, 32 | }); 33 | } 34 | }, []); 35 | 36 | useEffect(() => { 37 | scrollToBottom(); 38 | }, [messages, scrollToBottom]); 39 | 40 | const onFetchMessages = useCallback( 41 | (offset = 0, prepend = false) => { 42 | getMessages(roomId, offset).then(async (messages) => { 43 | /** We've got messages but it's possible we might not have the cached user entires which correspond to those messages */ 44 | await populateUsersFromLoadedMessages(state.users, dispatch, messages); 45 | 46 | dispatch({ 47 | type: prepend ? "prepend messages" : "set messages", 48 | payload: { id: roomId, messages: messages }, 49 | }); 50 | if (prepend) { 51 | setTimeout(() => { 52 | scrollToTop(); 53 | }, 10); 54 | } else { 55 | scrollToBottom(); 56 | } 57 | }); 58 | }, 59 | [dispatch, roomId, scrollToBottom, scrollToTop, state.users] 60 | ); 61 | 62 | useEffect(() => { 63 | if (roomId === undefined) { 64 | return; 65 | } 66 | if (messages === undefined) { 67 | /** Fetch logic goes here */ 68 | onFetchMessages(); 69 | } 70 | }, [ 71 | messages, 72 | dispatch, 73 | roomId, 74 | state.users, 75 | state, 76 | scrollToBottom, 77 | onFetchMessages, 78 | ]); 79 | 80 | useEffect(() => { 81 | if (messageListElement.current) { 82 | scrollToBottom(); 83 | } 84 | }, [scrollToBottom, roomId]); 85 | 86 | const onUserClicked = async (userId) => { 87 | /** Check if room exists. */ 88 | const targetUser = state.users[userId]; 89 | let roomId = targetUser.room; 90 | if (roomId === undefined) { 91 | // @ts-ignore 92 | const room = await addRoom(userId, user.id); 93 | roomId = room.id; 94 | /** We need to set this room id to user. */ 95 | dispatch({ type: "set user", payload: { ...targetUser, room: roomId } }); 96 | /** Then a new room should be added to the store. */ 97 | dispatch({ 98 | type: "add room", 99 | // @ts-ignore 100 | payload: { id: roomId, name: parseRoomName(room.names, user.username) }, 101 | }); 102 | } 103 | /** Then a room should be changed */ 104 | dispatch({ type: "set current room", payload: roomId }); 105 | }; 106 | 107 | const onLoadMoreMessages = useCallback(() => { 108 | onFetchMessages(room.offset, true); 109 | }, [onFetchMessages, room]); 110 | 111 | return { 112 | onLoadMoreMessages, 113 | onUserClicked, 114 | message, 115 | setMessage, 116 | dispatch, 117 | room, 118 | rooms: state.rooms, 119 | currentRoom: state.currentRoom, 120 | messageListElement, 121 | roomId, 122 | users: state.users, 123 | messages, 124 | }; 125 | }; 126 | export default useChatHandlers; -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/LoadingScreen.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | 4 | export function LoadingScreen() { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Login/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Toast } from "react-bootstrap"; 3 | import React, { useState, useRef } from "react"; 4 | import Logo from "../Logo"; 5 | import "./style.css"; 6 | import { useEffect } from "react"; 7 | 8 | const DEMO_USERS = ["Pablo", "Joe", "Mary", "Alex"]; 9 | 10 | export default function Login({ onLogIn }) { 11 | const [username, setUsername] = useState( 12 | () => DEMO_USERS[Math.floor(Math.random() * DEMO_USERS.length)] 13 | ); 14 | const [password, setPassword] = useState("password123"); 15 | const [error, setError] = useState(null); 16 | 17 | const onSubmit = async (event) => { 18 | event.preventDefault(); 19 | onLogIn(username, password, setError); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 |
31 |
32 |
43 |
44 |

Welcome Back !

45 |

Sign in to continue

46 |
47 |
48 | welcome back 53 |
54 |
55 |
59 |
67 | 68 |
69 |
70 |
71 | 72 |
81 | 82 | 83 |
84 | 89 |
90 | 91 | 94 | setPassword(event.target.value)} 97 | type="password" 98 | id="inputPassword" 99 | className="form-control" 100 | placeholder="Password" 101 | required 102 | /> 103 |
104 | 107 |
108 |
109 | setError(null)} 112 | show={error !== null} 113 | delay={3000} 114 | autohide 115 | > 116 | 117 | 122 | Error 123 | 124 | {error} 125 | 126 |
127 |
128 |
129 | 130 |
131 |
132 | 133 | ); 134 | } 135 | 136 | const UsernameSelect = ({ username, setUsername, names = [""] }) => { 137 | const [open, setOpen] = useState(false); 138 | const [width, setWidth] = useState(0); 139 | const ref = useRef(); 140 | /** @ts-ignore */ 141 | const clientRectWidth = ref.current?.getBoundingClientRect().width; 142 | useEffect(() => { 143 | /** @ts-ignore */ 144 | setWidth(clientRectWidth); 145 | }, [clientRectWidth]); 146 | 147 | /** Click away listener */ 148 | useEffect(() => { 149 | if (open) { 150 | const listener = () => setOpen(false); 151 | document.addEventListener("click", listener); 152 | return () => document.removeEventListener("click", listener); 153 | } 154 | }, [open]); 155 | 156 | /** Make the current div focused */ 157 | useEffect(() => { 158 | if (open) { 159 | /** @ts-ignore */ 160 | ref.current?.focus(); 161 | } 162 | }, [open]); 163 | 164 | return ( 165 |
setOpen((o) => !o)} 170 | > 171 |
172 |
{username}
173 |
174 | 175 | 176 | 177 |
178 |
179 |
183 | {names.map((name) => ( 184 |
setUsername(name)} 188 | > 189 | {name} 190 |
191 | ))} 192 |
193 |
194 | ); 195 | }; 196 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Login/style.css: -------------------------------------------------------------------------------- 1 | .login-form .username-select button { 2 | background-color: transparent !important; 3 | color: inherit; 4 | padding: 7.5px 12px !important; 5 | display: block !important; 6 | border: 1px solid rgb(206, 212, 218) !important; 7 | border-radius: 4px !important; 8 | width: 100% !important; 9 | text-align: left !important; 10 | } 11 | 12 | .login-form .username-select .btn-primary:not(:disabled):not(.disabled).active, 13 | .login-form .username-select .btn-primary:not(:disabled):not(.disabled):active, 14 | .show > .btn-primary.dropdown-toggle { 15 | color: inherit; 16 | } 17 | 18 | .login-form .username-select .dropdown-menu.show.dropdown-menu-right { 19 | transform: translate(0px, 38px) !important; 20 | } 21 | 22 | .username-select-dropdown { 23 | position: relative; 24 | display: flex !important; 25 | align-items: center; 26 | background-color: transparent !important; 27 | color: inherit; 28 | padding: 0 12px !important; 29 | border: 1px solid rgb(206, 212, 218) !important; 30 | border-radius: 4px !important; 31 | width: 100% !important; 32 | text-align: left !important; 33 | height: calc(1.5em + 0.94rem + 2px) !important; 34 | 35 | cursor: pointer; 36 | } 37 | 38 | .username-select-dropdown .username-select-block { 39 | background-color: var(--white); 40 | position: absolute; 41 | top: -1138px; 42 | left: 0; 43 | opacity: 0; 44 | transform: scale(0.5, 0.5); 45 | transform-origin: top left; 46 | transition: opacity 0.2s ease, transform 0.2s ease; 47 | 48 | border: 1px solid rgb(206, 212, 218) !important; 49 | border-radius: 4px !important; 50 | 51 | padding: 8px 0px; 52 | } 53 | 54 | .username-select-dropdown:focus { 55 | outline: none; 56 | border-color: #80bdff !important; 57 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 58 | } 59 | 60 | .username-select-dropdown .username-select-block.open { 61 | top: 42px; 62 | transform: scale(1, 1); 63 | opacity: 1; 64 | } 65 | 66 | .username-select-row { 67 | display: flex; 68 | width: 100%; 69 | justify-content: space-between; 70 | align-items: center; 71 | } 72 | 73 | .username-select-dropdown .username-select-block .username-select-block-item { 74 | padding: 4px 24px; 75 | } 76 | 77 | .username-select-dropdown 78 | .username-select-block 79 | .username-select-block-item:hover { 80 | background-color: var(--light); 81 | } 82 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | const Logo = ({ width = 64, height = 64 }) => { 2 | return ( 3 | 10 | 11 | 12 | 16 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default Logo; 49 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useEffect, useState } from "react"; 3 | import { getButtonLinks } from "../api"; 4 | 5 | const Navbar = () => { 6 | /** 7 | * @type {[{ 8 | * heroku?: string; 9 | * google_cloud?: string; 10 | * vercel?: string; 11 | * github?: string; 12 | * }, React.Dispatch]} 13 | */ 14 | const [links, setLinks] = useState(null); 15 | useEffect(() => { 16 | getButtonLinks().then(setLinks); 17 | }, []); 18 | return ( 19 | 29 | ); 30 | }; 31 | 32 | const GithubIcon = ({ link }) => ( 33 | 39 | 47 | 52 | 57 | 58 | 59 | ); 60 | 61 | export default Navbar; 62 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/hooks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useEffect, useState } from "react"; 3 | import { getMe, login, logOut } from "./api"; 4 | 5 | 6 | /** User management hook. */ 7 | const useUser = (onUserLoaded = (user) => { }, dispatch) => { 8 | const [loading, setLoading] = useState(true); 9 | /** @type {[import('./state.js').UserEntry | null, React.Dispatch]} */ 10 | const [user, setUser] = useState(null); 11 | /** Callback used in log in form. */ 12 | const onLogIn = ( 13 | username = "", 14 | password = "", 15 | onError = (val = null) => { }, 16 | onLoading = (loading = false) => { } 17 | ) => { 18 | onError(null); 19 | onLoading(true); 20 | login(username, password) 21 | .then((x) => { 22 | setUser(x); 23 | onLoading(false); 24 | }) 25 | .catch((e) => { 26 | onError(e.message); 27 | onLoading(false); 28 | }); 29 | }; 30 | 31 | /** Log out form */ 32 | const onLogOut = async () => { 33 | logOut().then(() => { 34 | setUser(null); 35 | /** This will clear the store, to completely re-initialize an app on the next login. */ 36 | dispatch({ type: "clear" }); 37 | setLoading(true); 38 | }); 39 | }; 40 | 41 | /** Runs once when the component is mounted to check if there's user stored in cookies */ 42 | useEffect(() => { 43 | if (!loading) { 44 | return; 45 | } 46 | getMe().then((user) => { 47 | setUser(user); 48 | setLoading(false); 49 | onUserLoaded(user); 50 | }); 51 | }, [onUserLoaded, loading]); 52 | 53 | return { user: typeof user === "string" ? null : user, onLogIn, onLogOut, loading }; 54 | }; 55 | 56 | export { 57 | useUser 58 | }; -------------------------------------------------------------------------------- /BasicRedisChat/client/src/index.jsx: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "./styles/style-overrides.css"; 3 | import "./styles/style.css"; 4 | import "./styles/font-face.css"; 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | import App from "./App"; 10 | 11 | ReactDOM.render(, document.getElementById("root")); 12 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/state.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createContext, useContext, useReducer } from "react"; 3 | 4 | /** 5 | * @typedef {{ 6 | * from: string 7 | * date: number 8 | * message: string 9 | * roomId?: string 10 | * }} Message 11 | * 12 | * @typedef {{ 13 | * name: string; 14 | * id: string; 15 | * messages?: Message[] 16 | * connected?: boolean; 17 | * offset?: number; 18 | * forUserId?: null | number | string 19 | * lastMessage?: Message | null 20 | * }} Room 21 | * 22 | * @typedef {{ 23 | * username: string; 24 | * id: string; 25 | * online?: boolean; 26 | * room?: string; 27 | * }} UserEntry 28 | * 29 | * @typedef {{ 30 | * currentRoom: string; 31 | * rooms: {[id: string]: Room}; 32 | * users: {[id: string]: UserEntry} 33 | * }} State 34 | * 35 | * @param {State} state 36 | * @param {{type: string; payload: any}} action 37 | * @returns {State} 38 | */ 39 | const reducer = (state, action) => { 40 | switch (action.type) { 41 | case "clear": 42 | return { currentRoom: "0", rooms: {}, users: {} }; 43 | case "set user": { 44 | return { 45 | ...state, 46 | users: { ...state.users, [action.payload.id]: action.payload }, 47 | }; 48 | } 49 | case "make user online": { 50 | return { 51 | ...state, 52 | users: { 53 | ...state.users, 54 | [action.payload]: { ...state.users[action.payload], online: true }, 55 | }, 56 | }; 57 | } 58 | case "append users": { 59 | return { ...state, users: { ...state.users, ...action.payload } }; 60 | } 61 | case "set messages": { 62 | return { 63 | ...state, 64 | rooms: { 65 | ...state.rooms, 66 | [action.payload.id]: { 67 | ...state.rooms[action.payload.id], 68 | messages: action.payload.messages, 69 | offset: action.payload.messages.length, 70 | }, 71 | }, 72 | }; 73 | } 74 | case "prepend messages": { 75 | const messages = [ 76 | ...action.payload.messages, 77 | ...state.rooms[action.payload.id].messages, 78 | ]; 79 | return { 80 | ...state, 81 | rooms: { 82 | ...state.rooms, 83 | [action.payload.id]: { 84 | ...state.rooms[action.payload.id], 85 | messages, 86 | offset: messages.length, 87 | }, 88 | }, 89 | }; 90 | } 91 | case "append message": 92 | if (state.rooms[action.payload.id] === undefined) { 93 | return state; 94 | } 95 | return { 96 | ...state, 97 | rooms: { 98 | ...state.rooms, 99 | [action.payload.id]: { 100 | ...state.rooms[action.payload.id], 101 | lastMessage: action.payload.message, 102 | messages: state.rooms[action.payload.id].messages 103 | ? [ 104 | ...state.rooms[action.payload.id].messages, 105 | action.payload.message, 106 | ] 107 | : undefined, 108 | }, 109 | }, 110 | }; 111 | case 'set last message': 112 | return { ...state, rooms: { ...state.rooms, [action.payload.id]: { ...state.rooms[action.payload.id], lastMessage: action.payload.lastMessage } } }; 113 | case "set current room": 114 | return { ...state, currentRoom: action.payload }; 115 | case "add room": 116 | return { 117 | ...state, 118 | rooms: { ...state.rooms, [action.payload.id]: action.payload }, 119 | }; 120 | case "set rooms": { 121 | /** @type {Room[]} */ 122 | const newRooms = action.payload; 123 | const rooms = { ...state.rooms }; 124 | newRooms.forEach((room) => { 125 | rooms[room.id] = { 126 | ...room, 127 | messages: rooms[room.id] && rooms[room.id].messages, 128 | }; 129 | }); 130 | return { ...state, rooms }; 131 | } 132 | default: 133 | return state; 134 | } 135 | }; 136 | 137 | /** @type {State} */ 138 | const initialState = { 139 | currentRoom: "main", 140 | rooms: {}, 141 | users: {}, 142 | }; 143 | 144 | const useAppStateContext = () => { 145 | return useReducer(reducer, initialState); 146 | }; 147 | 148 | // @ts-ignore 149 | export const AppContext = createContext(); 150 | 151 | /** 152 | * @returns {[ 153 | * State, 154 | * React.Dispatch<{ 155 | * type: string; 156 | * payload: any; 157 | * }> 158 | * ]} 159 | */ 160 | export const useAppState = () => { 161 | const [state, dispatch] = useContext(AppContext); 162 | return [state, dispatch]; 163 | }; 164 | 165 | export default useAppStateContext; -------------------------------------------------------------------------------- /BasicRedisChat/client/src/styles/font-face.css: -------------------------------------------------------------------------------- 1 | /* devanagari */ 2 | @font-face { 3 | font-family: "Poppins"; 4 | font-style: normal; 5 | font-weight: 300; 6 | font-display: swap; 7 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2) 8 | format("woff2"); 9 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 10 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 11 | } 12 | /* latin-ext */ 13 | @font-face { 14 | font-family: "Poppins"; 15 | font-style: normal; 16 | font-weight: 300; 17 | font-display: swap; 18 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2) 19 | format("woff2"); 20 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 21 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 22 | } 23 | /* latin */ 24 | @font-face { 25 | font-family: "Poppins"; 26 | font-style: normal; 27 | font-weight: 300; 28 | font-display: swap; 29 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2) 30 | format("woff2"); 31 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 32 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 33 | U+FEFF, U+FFFD; 34 | } 35 | /* devanagari */ 36 | @font-face { 37 | font-family: "Poppins"; 38 | font-style: normal; 39 | font-weight: 400; 40 | font-display: swap; 41 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) 42 | format("woff2"); 43 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 44 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 45 | } 46 | /* latin-ext */ 47 | @font-face { 48 | font-family: "Poppins"; 49 | font-style: normal; 50 | font-weight: 400; 51 | font-display: swap; 52 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) 53 | format("woff2"); 54 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 55 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 56 | } 57 | /* latin */ 58 | @font-face { 59 | font-family: "Poppins"; 60 | font-style: normal; 61 | font-weight: 400; 62 | font-display: swap; 63 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) 64 | format("woff2"); 65 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 66 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 67 | U+FEFF, U+FFFD; 68 | } 69 | /* devanagari */ 70 | @font-face { 71 | font-family: "Poppins"; 72 | font-style: normal; 73 | font-weight: 500; 74 | font-display: swap; 75 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2) 76 | format("woff2"); 77 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 78 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 79 | } 80 | /* latin-ext */ 81 | @font-face { 82 | font-family: "Poppins"; 83 | font-style: normal; 84 | font-weight: 500; 85 | font-display: swap; 86 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2) 87 | format("woff2"); 88 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 89 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 90 | } 91 | /* latin */ 92 | @font-face { 93 | font-family: "Poppins"; 94 | font-style: normal; 95 | font-weight: 500; 96 | font-display: swap; 97 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2) 98 | format("woff2"); 99 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 100 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 101 | U+FEFF, U+FFFD; 102 | } 103 | /* devanagari */ 104 | @font-face { 105 | font-family: "Poppins"; 106 | font-style: normal; 107 | font-weight: 600; 108 | font-display: swap; 109 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2) 110 | format("woff2"); 111 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 112 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 113 | } 114 | /* latin-ext */ 115 | @font-face { 116 | font-family: "Poppins"; 117 | font-style: normal; 118 | font-weight: 600; 119 | font-display: swap; 120 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2) 121 | format("woff2"); 122 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 123 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 124 | } 125 | /* latin */ 126 | @font-face { 127 | font-family: "Poppins"; 128 | font-style: normal; 129 | font-weight: 600; 130 | font-display: swap; 131 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2) 132 | format("woff2"); 133 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 134 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 135 | U+FEFF, U+FFFD; 136 | } 137 | /* devanagari */ 138 | @font-face { 139 | font-family: "Poppins"; 140 | font-style: normal; 141 | font-weight: 700; 142 | font-display: swap; 143 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2) 144 | format("woff2"); 145 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 146 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 147 | } 148 | /* latin-ext */ 149 | @font-face { 150 | font-family: "Poppins"; 151 | font-style: normal; 152 | font-weight: 700; 153 | font-display: swap; 154 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2) 155 | format("woff2"); 156 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 157 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 158 | } 159 | /* latin */ 160 | @font-face { 161 | font-family: "Poppins"; 162 | font-style: normal; 163 | font-weight: 700; 164 | font-display: swap; 165 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) 166 | format("woff2"); 167 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 168 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 169 | U+FEFF, U+FFFD; 170 | } 171 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/styles/style-overrides.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: rgb(85, 110, 230) !important; 3 | --light: #f5f5f8 !important; 4 | --success: rgb(52, 195, 143) !important; 5 | } 6 | 7 | .bg-success { 8 | background-color: var(--success) !important; 9 | } 10 | 11 | .bg-light { 12 | background-color: var(--light) !important; 13 | } 14 | 15 | .bg-gray { 16 | background-color: var(--gray) !important; 17 | } 18 | 19 | .bg-primary { 20 | background-color: var(--primary) !important; 21 | } 22 | 23 | .text-primary { 24 | color: var(--primary) !important; 25 | } 26 | 27 | .list-group-item.active { 28 | background-color: var(--primary) !important; 29 | border-color: var(--primary) !important; 30 | } 31 | 32 | .btn-rounded { 33 | border-radius: 30px !important; 34 | } 35 | 36 | .btn { 37 | display: inline-block; 38 | font-weight: 400; 39 | color: #495057; 40 | text-align: center; 41 | vertical-align: middle; 42 | user-select: none; 43 | background-color: transparent; 44 | border: 1px solid transparent; 45 | border-radius: 30px !important; 46 | padding: 0.47rem 0.75rem; 47 | font-size: 0.8125rem; 48 | line-height: 1.5; 49 | border-radius: 0.25rem; 50 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 51 | border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; 52 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 53 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 54 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 55 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, 56 | -webkit-box-shadow 0.15s ease-in-out; 57 | } 58 | 59 | .btn-primary { 60 | color: #fff; 61 | background-color: var(--primary); 62 | border-color: var(--primary); 63 | } 64 | 65 | .btn-primary.focus, 66 | .btn-primary:focus, 67 | .btn-primary:hover { 68 | color: #fff; 69 | background-color: #3452e1; 70 | border-color: #2948df; 71 | } 72 | 73 | .font-size-14 { 74 | font-size: 14px !important; 75 | } 76 | 77 | .font-size-11 { 78 | font-size: 11px !important; 79 | } 80 | 81 | .font-size-12 { 82 | font-size: 12px !important; 83 | } 84 | 85 | .font-size-15 { 86 | font-size: 15px !important; 87 | } 88 | 89 | .w-md { 90 | min-width: 110px; 91 | } 92 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/styles/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Poppins", Arial, Helvetica, sans-serif; 3 | font-size: 13px; 4 | color: #495057; 5 | } 6 | 7 | .navbar { 8 | box-shadow: rgba(18, 38, 63, 0.03) 0px 12px 24px 0px; 9 | } 10 | 11 | .navbar-brand { 12 | font-size: 16px; 13 | } 14 | 15 | .chats-title { 16 | padding-left: 14px; 17 | } 18 | 19 | .login-page { 20 | display: flex; 21 | align-items: center; 22 | flex-direction: column; 23 | justify-content: center; 24 | padding-bottom: 190px; 25 | height: 100vh; 26 | } 27 | 28 | .form-signin { 29 | width: 100%; 30 | max-width: 330px; 31 | padding: 15px; 32 | margin: 0 auto; 33 | } 34 | 35 | .text-small { 36 | font-size: 0.9rem; 37 | } 38 | 39 | .messages-box, 40 | .chat-box { 41 | /* height: 510px; */ 42 | width: 100%; 43 | } 44 | 45 | .chat-box-wrapper { 46 | flex: 1; 47 | overflow-y: scroll; 48 | } 49 | 50 | .rounded-lg { 51 | border-radius: 0.5rem; 52 | } 53 | 54 | input::placeholder { 55 | font-size: 0.9rem; 56 | color: #999; 57 | } 58 | 59 | .centered-box { 60 | width: 100%; 61 | height: 100vh; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | } 66 | 67 | .login-error-anchor { 68 | position: relative; 69 | } 70 | 71 | .toast-box { 72 | text-align: left; 73 | margin-top: 30px; 74 | position: absolute; 75 | width: 100%; 76 | top: 0; 77 | display: flex; 78 | flex-direction: row; 79 | justify-content: center; 80 | } 81 | 82 | .full-height { 83 | height: 100vh; 84 | flex-direction: column; 85 | display: flex; 86 | } 87 | 88 | .full-height .container { 89 | flex: 1; 90 | } 91 | 92 | .container .row { 93 | height: 100%; 94 | } 95 | 96 | .flex-column { 97 | display: flex; 98 | flex-direction: column; 99 | } 100 | 101 | .bg-white.flex-column { 102 | height: 100%; 103 | } 104 | 105 | .flex { 106 | flex: 1; 107 | } 108 | 109 | .logout-button { 110 | cursor: pointer; 111 | display: flex; 112 | flex-direction: row; 113 | align-items: center; 114 | padding: 15px 20px; 115 | } 116 | 117 | .logout-button svg { 118 | margin-right: 15px; 119 | } 120 | 121 | .no-messages { 122 | opacity: 0.5; 123 | height: 100%; 124 | width: 100%; 125 | } 126 | 127 | .avatar-box { 128 | width: 50px; 129 | height: 50px; 130 | object-fit: cover; 131 | object-position: 50%; 132 | overflow: hidden; 133 | border-radius: 4px; 134 | cursor: pointer; 135 | } 136 | 137 | .user-link { 138 | cursor: pointer; 139 | } 140 | 141 | .user-link:hover { 142 | text-decoration: underline; 143 | } 144 | 145 | .online-indicator { 146 | width: 14px; 147 | height: 14px; 148 | border: 2px solid white; 149 | bottom: -7px; 150 | right: -7px; 151 | background-color: #4df573; 152 | } 153 | 154 | .online-indicator.selected { 155 | border: none; 156 | width: 12px; 157 | height: 12px; 158 | bottom: -5px; 159 | right: -5px; 160 | } 161 | 162 | .online-indicator.offline { 163 | background-color: #bbb; 164 | } 165 | 166 | span.pseudo-link { 167 | font-size: 14px; 168 | text-decoration: underline; 169 | color: var(--blue); 170 | cursor: pointer; 171 | } 172 | 173 | span.pseudo-link:hover { 174 | text-decoration: none; 175 | } 176 | 177 | .list-group-item { 178 | cursor: pointer; 179 | height: 70px; 180 | box-sizing: border-box; 181 | transition: background-color 0.1s ease-out; 182 | } 183 | 184 | .chat-icon { 185 | width: 45px; 186 | height: 45px; 187 | border-radius: 4px; 188 | background-color: #eee; 189 | } 190 | 191 | .chat-icon.active { 192 | background-color: var(--blue); 193 | } 194 | 195 | .chats-title { 196 | font-size: 15px; 197 | } 198 | 199 | .chat-body { 200 | border-radius: 10px !important; 201 | } 202 | 203 | .chat-list-container { 204 | height: 100%; 205 | } 206 | 207 | .chat-input { 208 | border-radius: 30px !important; 209 | background-color: #eff2f7 !important; 210 | border-color: #eff2f7 !important; 211 | padding-right: 120px; 212 | } 213 | 214 | .form-control::placeholder { 215 | font-size: 13px; 216 | } 217 | 218 | .form-control { 219 | display: block; 220 | width: 100%; 221 | height: calc(1.5em + 0.94rem + 2px); 222 | padding: 7.5px 12px; 223 | font-size: 13px; 224 | font-weight: 400; 225 | line-height: 1.5; 226 | color: #495057; 227 | background-color: #fff; 228 | background-clip: padding-box; 229 | border: 1px solid #ced4da; 230 | -webkit-transition: border-color 0.15s ease-in-out, 231 | -webkit-box-shadow 0.15s ease-in-out; 232 | transition: border-color 0.15s ease-in-out, 233 | -webkit-box-shadow 0.15s ease-in-out; 234 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 235 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, 236 | -webkit-box-shadow 0.15s ease-in-out; 237 | } 238 | 239 | .rounded-button { 240 | border-radius: 30px; 241 | background-color: var(--light); 242 | } 243 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/use-socket.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useEffect, useRef, useState } from "react"; 3 | // eslint-disable-next-line no-unused-vars 4 | import io, { Socket } from "socket.io-client"; 5 | import { parseRoomName } from "./utils"; 6 | import * as signalR from '@microsoft/signalr'; 7 | /** 8 | * @param {import('./state').UserEntry} newUser 9 | */ 10 | const updateUser = (newUser, dispatch, infoMessage) => { 11 | dispatch({ type: "set user", payload: newUser }); 12 | if (infoMessage !== undefined) { 13 | dispatch({ 14 | type: "append message", 15 | payload: { 16 | id: "0", 17 | message: { 18 | /** Date isn't shown in the info message, so we only need a unique value */ 19 | date: Math.random() * 10000, 20 | from: "info", 21 | message: infoMessage, 22 | }, 23 | }, 24 | }); 25 | } 26 | }; 27 | 28 | /** @returns {[Socket, boolean, () => void]} */ 29 | const useSocket = (user, dispatch, onLogOut) => { 30 | const [connected, setConnected] = useState(false); 31 | /** @type {React.MutableRefObject} */ 32 | const socketRef = useRef(null); 33 | const socket = socketRef.current; 34 | 35 | /** First of all it's necessary to handle the socket io connection */ 36 | useEffect(() => { 37 | if (user === null) { 38 | if (socket !== null) { 39 | socketRef.current.stop().then(() => { 40 | socketRef.current = null; 41 | setConnected(false); 42 | }); 43 | } 44 | } else { 45 | if (socketRef.current === null) { 46 | socketRef.current = new signalR.HubConnectionBuilder() 47 | .withUrl("/chat") 48 | .build(); 49 | socketRef.current.start().then(() => { 50 | debugger 51 | //socketRef.current.invoke("OnLogIn", JSON.stringify(user)); 52 | setConnected(true); 53 | }); 54 | } 55 | } 56 | }, [user, socket]); 57 | 58 | /** 59 | * Once we are sure the socket io object is initialized 60 | * Add event listeners. 61 | */ 62 | useEffect(() => { 63 | if (connected && user) { 64 | socket.on("user.connected", (username) => { 65 | updateUser(username, dispatch, `${username} connected`); 66 | }); 67 | socket.on("user.disconnected", (username) => { 68 | updateUser(username, dispatch, `${username} left`); 69 | }); 70 | socket.on("message", (message) => { 71 | /** Set user online */ 72 | message = JSON.parse(message); 73 | 74 | dispatch({ 75 | type: "make user online", 76 | payload: message.from, 77 | }); 78 | dispatch({ 79 | type: "append message", 80 | payload: { id: message.roomId === undefined ? "0" : message.roomId, message }, 81 | }); 82 | }); 83 | } else { 84 | /** If there was a log out, we need to clear existing listeners on an active socket connection */ 85 | if (socket) { 86 | socket.off("user.connected"); 87 | socket.off("user.disconnected"); 88 | socket.off("user.room"); 89 | socket.off("message"); 90 | } 91 | } 92 | }, [connected, user, dispatch, socket]); 93 | 94 | return [{ 95 | // @ts-ignore 96 | emit(_, message) { 97 | socket.invoke("SendMessage", JSON.stringify(user), JSON.stringify(message)); 98 | return {}; 99 | } 100 | }, true, 101 | () => { 102 | //socket.invoke("OnLogOut", JSON.stringify(user)); 103 | onLogOut(); 104 | }]; 105 | }; 106 | 107 | export { useSocket }; 108 | -------------------------------------------------------------------------------- /BasicRedisChat/client/src/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getUsers } from "./api"; 4 | 5 | /** 6 | * @param {string[]} names 7 | * @param {string} username 8 | */ 9 | export const parseRoomName = (names, username) => { 10 | for (let name of names) { 11 | if (typeof name !== 'string') { 12 | name = name[0]; 13 | } 14 | if (name !== username) { 15 | return name; 16 | } 17 | } 18 | return names[0]; 19 | }; 20 | 21 | /** Get an avatar for a room or a user */ 22 | export const getAvatarByUserAndRoomId = (roomId = "1") => { 23 | const TOTAL_IMAGES = 13; 24 | const seed1 = 654; 25 | const seed2 = 531; 26 | 27 | const uidParsed = +roomId.split(":").pop(); 28 | let roomIdParsed = +roomId.split(":").reverse().pop(); 29 | if (roomIdParsed < 0) { 30 | roomIdParsed += 3555; 31 | } 32 | 33 | const theId = (uidParsed * seed1 + roomIdParsed * seed2) % TOTAL_IMAGES; 34 | 35 | return `${process.env.PUBLIC_URL}/avatars/${theId}.jpg`; 36 | }; 37 | 38 | const jdenticon = require("jdenticon"); 39 | 40 | const avatars = {}; 41 | export const getAvatar = (username) => { 42 | let av = avatars[username]; 43 | if (av === undefined) { 44 | av = 45 | "data:image/svg+xml;base64," + window.btoa(jdenticon.toSvg(username, 50)); 46 | avatars[username] = av; 47 | } 48 | return av; 49 | }; 50 | 51 | export const populateUsersFromLoadedMessages = async (users, dispatch, messages) => { 52 | const userIds = {}; 53 | messages.forEach((message) => { 54 | userIds[message.from] = 1; 55 | }); 56 | 57 | const ids = Object.keys(userIds).filter( 58 | (id) => users[id] === undefined 59 | ); 60 | 61 | if (ids.length !== 0) { 62 | /** We need to fetch users first */ 63 | const newUsers = await getUsers(ids); 64 | dispatch({ 65 | type: "append users", 66 | payload: newUsers, 67 | }); 68 | } 69 | 70 | }; -------------------------------------------------------------------------------- /BasicRedisChat/repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": "https://github.com/redis-developer/basic-redis-chat-app-demo-dotnet" 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 2 | WORKDIR /app 3 | 4 | EXPOSE 80 5 | 6 | ENV PORT = 80 7 | ENV REDIS_ENDPOINT_URL = "Redis server URI" 8 | ENV REDIS_PASSWORD = "Password to the server" 9 | 10 | FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS build 11 | WORKDIR /src 12 | COPY . . 13 | RUN dotnet restore "BasicRedisChat/BasicRedisChat.csproj" 14 | 15 | WORKDIR "/src/BasicRedisChat" 16 | RUN dotnet build "BasicRedisChat.csproj" -c Release -o /app 17 | 18 | FROM build AS publish 19 | RUN dotnet publish "BasicRedisChat.csproj" -c Release -o /app 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app . 24 | COPY --from=build /src/BasicRedisChat/client/build ./client/build 25 | 26 | ENTRYPOINT ["dotnet", "BasicRedisChat.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redis Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basic Redis Chat App Demo C# (.Net Core 5) 2 | 3 | Showcases how to implement chat app with ASP.NET Core, SignalR and Redis. This example uses the [pub/sub](https://redis.io/topics/pubsub) feature combined with Websockets from [SignalR](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-5.0#signalr) for implementing the message communication between client and server. 4 | 5 | 6 | 7 | 8 | ## Overview video 9 | 10 | Here's a short video that explains the project and how it uses Redis: 11 | 12 | [![Watch the video on YouTube](https://github.com/redis-developer/basic-redis-chat-app-demo-dotnet/raw/main/docs/YTThumbnail.png)](https://www.youtube.com/watch?v=miK7xDkDXF0) 13 | 14 | ## Technical Stack 15 | 16 | - Frontend - _React_, _Socket_ (@microsoft/signalr) 17 | - Backend - _.Net Core 5.0_, _Redis_ (Microsoft.Extensions.Caching.StackExchangeRedis) 18 | 19 | ## How it works? 20 | 21 | ### Database Schema 22 | 23 | #### User 24 | 25 | ```csharp 26 | public class User : BaseEntity 27 | { 28 | public int Id { get; set; } 29 | public string Username { get; set; } 30 | public bool Online { get; set; } = false; 31 | } 32 | ``` 33 | 34 | #### ChatRoom 35 | 36 | ```csharp 37 | public class ChatRoom : BaseEntity 38 | { 39 | public string Id { get; set; } 40 | public IEnumerable Names { get; set; } 41 | } 42 | ``` 43 | 44 | #### ChatRoomMessage 45 | 46 | ```csharp 47 | public class ChatRoomMessage : BaseEntity 48 | { 49 | public string From { get; set; } 50 | public int Date { get; set; } 51 | public string Message { get; set; } 52 | public string RoomId { get; set; } 53 | } 54 | ``` 55 | 56 | ### Initialization 57 | 58 | For simplicity, a key with **total_users** value is checked: if it does not exist, we fill the Redis database with initial data. 59 | `EXISTS total_users` (checks if the key exists) 60 | 61 | The demo data initialization is handled in multiple steps: 62 | 63 | **Creating of demo users:** 64 | We create a new user id: `INCR total_users`. Then we set a user ID lookup key by user name: **_e.g._** `SET username:nick user:1`. And finally, the rest of the data is written to the hash set: **_e.g._** `HSET user:1 username "nick" password "bcrypt_hashed_password"`. 65 | 66 | Additionally, each user is added to the default "General" room. For handling rooms for each user, we have a set that holds the room ids. Here's an example command of how to add the room: **_e.g._** `SADD user:1:rooms "0"`. 67 | 68 | **Populate private messages between users.** 69 | At first, private rooms are created: if a private room needs to be established, for each user a room id: `room:1:2` is generated, where numbers correspond to the user ids in ascending order. 70 | 71 | **_E.g._** Create a private room between 2 users: `SADD user:1:rooms 1:2` and `SADD user:2:rooms 1:2`. 72 | 73 | Then we add messages to this room by writing to a sorted set: 74 | 75 | **_E.g._** `ZADD room:1:2 1615480369 "{'from': 1, 'date': 1615480369, 'message': 'Hello', 'roomId': '1:2'}"`. 76 | 77 | We use a stringified _JSON_ for keeping the message structure and simplify the implementation details for this demo-app. 78 | 79 | **Populate the "General" room with messages.** Messages are added to the sorted set with id of the "General" room: `room:0` 80 | 81 | ### Registration 82 | 83 | ![How it works](docs/screenshot000.png) 84 | 85 | Redis is used mainly as a database to keep the user/messages data and for sending messages between connected servers. 86 | 87 | #### How the data is stored: 88 | 89 | - The chat data is stored in various keys and various data types. 90 | - User data is stored in a hash set where each user entry contains the next values: 91 | - `username`: unique user name; 92 | - `password`: hashed password 93 | 94 | * User hash set is accessed by key `user:{userId}`. The data for it stored with `HSET key field data`. User id is calculated by incrementing the `total_users`. 95 | 96 | - E.g `INCR total_users` 97 | 98 | * Username is stored as a separate key (`username:{username}`) which returns the userId for quicker access. 99 | - E.g `SET username:Alex 4` 100 | 101 | #### How the data is accessed: 102 | 103 | - **Get User** `HGETALL user:{id}` 104 | 105 | - E.g `HGETALL user:2`, where we get data for the user with id: 2. 106 | 107 | - **Online users:** will return ids of users which are online 108 | - E.g `SMEMBERS online_users` 109 | 110 | #### Code Example: Prepare User Data in Redis HashSet 111 | 112 | ```csharp 113 | var usernameKey = $"username:{username}"; 114 | // Yeah, bcrypt generally ins't used in .NET, this one is mainly added to be compatible with Node and Python demo servers. 115 | var hashedPassword = BCrypt.Net.BCrypt.HashPassword(password); 116 | var nextId = await redisDatabase.StringIncrementAsync("total_users"); 117 | var userKey = $"user:{nextId}"; 118 | await redisDatabase.StringSetAsync(usernameKey, userKey); 119 | await redisDatabase.HashSetAsync(userKey, new HashEntry[] { 120 | new HashEntry("username", username), 121 | new HashEntry("password", hashedPassword) 122 | }); 123 | ``` 124 | 125 | ### Rooms 126 | 127 | ![How it works](docs/screenshot001.png) 128 | 129 | #### How the data is stored: 130 | 131 | Each user has a set of rooms associated with them. 132 | 133 | **Rooms** are sorted sets which contains messages where score is the timestamp for each message. Each room has a name associated with it. 134 | 135 | - Rooms which user belongs too are stored at `user:{userId}:rooms` as a set of room ids. 136 | 137 | - E.g `SADD user:Alex:rooms 1` 138 | 139 | - Set room name: `SET room:{roomId}:name {name}` 140 | - E.g `SET room:1:name General` 141 | 142 | #### How the data is accessed: 143 | 144 | - **Get room name** `GET room:{roomId}:name`. 145 | 146 | - E. g `GET room:0:name`. This should return "General" 147 | 148 | - **Get room ids of a user:** `SMEMBERS user:{id}:rooms`. 149 | - E. g `SMEMBERS user:2:rooms`. This will return IDs of rooms for user with ID: 2 150 | 151 | #### Code Example: Get all My Rooms 152 | 153 | ```csharp 154 | //fetch all my rooms 155 | var roomIds = await _database.SetMembersAsync($"user:{userId}:rooms"); 156 | var rooms = new List(); 157 | foreach (var roomIdRedisValue in roomIds) 158 | { 159 | var roomId = roomIdRedisValue.ToString(); 160 | //fetch all users in the rooms 161 | var name = await _database.StringGetAsync($"room:{roomId}:name"); 162 | if (name.IsNullOrEmpty) 163 | { 164 | // It's a room without a name, likey the one with private messages 165 | var roomExists = await _database.KeyExistsAsync($"room:{roomId}"); 166 | if (!roomExists) 167 | { 168 | continue; 169 | } 170 | 171 | var userIds = roomId.Split(':'); 172 | if (userIds.Length != 2) 173 | { 174 | throw new Exception("You don't have access to this room"); 175 | } 176 | 177 | rooms.Add(new ChatRoom() 178 | { 179 | Id = roomId, 180 | Names = new List() { 181 | (await _database.HashGetAsync($"user:{userIds[0]}", "username")).ToString(), 182 | (await _database.HashGetAsync($"user:{userIds[1]}", "username")).ToString(), 183 | } 184 | }); 185 | } 186 | else 187 | { 188 | rooms.Add(new ChatRoom() 189 | { 190 | Id = roomId, 191 | Names = new List() { 192 | name.ToString() 193 | } 194 | }); 195 | } 196 | } 197 | return rooms; 198 | ``` 199 | 200 | ### Messages 201 | 202 | #### Pub/sub 203 | 204 | After initialization, a pub/sub subscription is created: `SUBSCRIBE MESSAGES`. At the same time, each server instance will run a listener on a message on this channel to receive real-time updates. 205 | 206 | Again, for simplicity, each message is serialized to **_JSON_**, which we parse and then handle in the same manner, as WebSocket messages. 207 | 208 | Pub/sub allows connecting multiple servers written in different platforms without taking into consideration the implementation detail of each server. 209 | 210 | #### How the data is stored: 211 | 212 | - Messages are stored at `room:{roomId}` key in a sorted set (as mentioned above). They are added with `ZADD room:{roomId} {timestamp} {message}` command. Message is serialized to an app-specific JSON string. 213 | - E.g `ZADD room:0 1617197047 { "From": "2", "Date": 1617197047, "Message": "Hello", "RoomId": "1:2" }` 214 | 215 | #### How the data is accessed: 216 | 217 | - **Get list of messages** `ZREVRANGE room:{roomId} {offset_start} {offset_end}`. 218 | - E.g `ZREVRANGE room:1:2 0 50` will return 50 messages with 0 offsets for the private room between users with IDs 1 and 2. 219 | 220 | #### Code Example: Send Message 221 | 222 | ```csharp 223 | public async Task SendMessage(UserDto user, ChatRoomMessage message) 224 | { 225 | await _database.SetAddAsync("online_users", message.From); 226 | var roomKey = $"room:{message.RoomId}"; 227 | await _database.SortedSetAddAsync(roomKey, JsonConvert.SerializeObject(message), (double)message.Date); 228 | await PublishMessage("message", message); 229 | } 230 | ``` 231 | 232 | ### Session handling 233 | 234 | The chat server works as a basic _REST_ API which involves keeping the session and handling the user state in the chat rooms (besides the WebSocket/real-time part). 235 | 236 | When a WebSocket/real-time server is instantiated, which listens for the next events: 237 | 238 | **Connection**. A new user is connected. At this point, a user ID is captured and saved to the session (which is cached in Redis). Note, that session caching is language/library-specific and it's used here purely for persistence and maintaining the state between server reloads. 239 | 240 | A global set with `online_users` key is used for keeping the online state for each user. So on a new connection, a user ID is written to that set: 241 | 242 | **E.g.** `SADD online_users 1` (We add user with id 1 to the set **online_users**). 243 | 244 | After that, a message is broadcasted to the clients to notify them that a new user is joined the chat. 245 | 246 | **Disconnect**. It works similarly to the connection event, except we need to remove the user for **online_users** set and notify the clients: `SREM online_users 1` (makes user with id 1 offline). 247 | 248 | **Message**. A user sends a message, and it needs to be broadcasted to the other clients. The pub/sub allows us also to broadcast this message to all server instances which are connected to this Redis: 249 | 250 | `PUBLISH message "{'serverId': 4132, 'type':'message', 'data': {'from': 1, 'date': 1615480369, 'message': 'Hello', 'roomId': '1:2'}}"` 251 | 252 | Note we send additional data related to the type of the message and the server id. Server id is used to discard the messages by the server instance which sends them since it is connected to the same `MESSAGES` channel. 253 | 254 | `type` field of the serialized JSON corresponds to the real-time method we use for real-time communication (connect/disconnect/message). 255 | 256 | `data` is method-specific information. In the example above it's related to the new message. 257 | 258 | #### How the data is stored / accessed: 259 | 260 | The session data is stored in Redis by utilizing the [**StackExchange.Redis**](https://github.com/StackExchange/StackExchange.Redis) client. 261 | 262 | ```csharp 263 | services 264 | .AddDataProtection() 265 | .PersistKeysToStackExchangeRedis(redis, "DataProtectionKeys"); 266 | 267 | services.AddStackExchangeRedisCache(option => 268 | { 269 | option.Configuration = redisConnectionUrl; 270 | option.InstanceName = "RedisInstance"; 271 | }); 272 | 273 | services.AddSession(options => 274 | { 275 | options.IdleTimeout = TimeSpan.FromMinutes(30); 276 | options.Cookie.Name = "AppTest"; 277 | }); 278 | ``` 279 | 280 | ## How to run it locally? 281 | 282 | #### Write in environment variable or Dockerfile actual connection to Redis: 283 | 284 | ``` 285 | REDIS_ENDPOINT_URL = "Redis server URI:PORT_NUMBER" 286 | REDIS_PASSWORD = "Password to the server" 287 | ``` 288 | 289 | #### Run backend 290 | 291 | Build the Redis container with the following command: 292 | ```sh 293 | docker-compose up -d 294 | ``` 295 | 296 | From the _BasicRedisChat_ Directory execute: 297 | 298 | ```sh 299 | dotnet run 300 | ``` 301 | 302 | ## Try it out 303 | 304 | #### Deploy to Heroku 305 | 306 |

307 | 308 | Deploy to Heorku 309 | 310 |

311 | 312 | #### Deploy to Google Cloud 313 | 314 |

315 | 316 | Run on Google Cloud 317 | 318 |

319 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic Redis Chat App Demo .Net Core 5", 3 | "logo": "https://redis.io/images/redis-white.png", 4 | "keywords": ["python", "py", "redis"], 5 | "addons": ["rediscloud:30"], 6 | "buildpacks": [{ 7 | "url": "heroku/python" 8 | }], 9 | "stack": "container", 10 | "env": { 11 | "REDIS_ENDPOINT_URL": { 12 | "description": "Redis server host:port, ex: 127.0.0.1:6379", 13 | "required": true 14 | }, 15 | "REDIS_PASSWORD": { 16 | "description": "Redis server password", 17 | "required": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | redis: 4 | container_name: basic-redis-chat 5 | image: redis 6 | restart: always 7 | command: redis-server --appendonly yes --requirepass 123456 8 | ports: 9 | - "6379:6379" 10 | -------------------------------------------------------------------------------- /docs/YTThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/docs/YTThumbnail.png -------------------------------------------------------------------------------- /docs/screenshot000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/docs/screenshot000.png -------------------------------------------------------------------------------- /docs/screenshot001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/docs/screenshot001.png -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | -------------------------------------------------------------------------------- /images/app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/425f981445286789f2fa875bed257202d1b810fa/images/app_preview_image.png -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Basic .NET Core Redis chat", 3 | "description": "Showcases how to implement chat app in .NET Core, SignalR and Redis", 4 | "type": "App", 5 | "contributed_by": "Redis", 6 | "repo_url": "https://github.com/redis-developer/basic-redis-chat-app-demo-dotnet", 7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/master/images/app_preview_image.png", 8 | "download_url": "https://github.com/redis-developer/basic-redis-chat-app-demo-dotnet/archive/main.zip", 9 | "hosted_url": "", 10 | "quick_deploy": "true", 11 | "deploy_buttons": [ 12 | { 13 | "heroku": "https://heroku.com/deploy?template=https://github.com/redis-developer/basic-redis-chat-app-demo-dotnet" 14 | }, 15 | { 16 | "Google": "https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-chat-app-demo-dotnet.git" 17 | } 18 | ], 19 | "language": [ 20 | "C#" 21 | ], 22 | "redis_commands": [ 23 | "AUTH", 24 | "INCR", 25 | "DECR", 26 | "HMSET", 27 | "EXISTS", 28 | "HEXISTS", 29 | "SET", 30 | "GET", 31 | "HGETALL", 32 | "ZRANGEBYSCORE", 33 | "ZADD", 34 | "SADD", 35 | "HMGET", 36 | "SISMEMBER", 37 | "SMEMBERS", 38 | "SREM", 39 | "PUBLISH", 40 | "SUBSCRIBE" 41 | ], 42 | "redis_use_cases": [ 43 | "Pub/Sub" 44 | ], 45 | "redis_features": [], 46 | "app_image_urls": [ 47 | "https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/main/docs/screenshot001.png" 48 | ], 49 | "youtube_url": "https://www.youtube.com/watch?v=miK7xDkDXF0", 50 | "special_tags": [], 51 | "verticals": [], 52 | "markdown": "https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-dotnet/main/README.md" 53 | } --------------------------------------------------------------------------------