liebus

liebus

How to store "message history" & list of ChatRoom participants (using join/leave functions)?

Hi. I am learning Elixir recently, decided to make a small simple project “Chat Room” for myself to learn some OTP. Please help to implement “message history” (to store 100 messages for example) and list of chat room participants (using join/leave functions).

I do not understand clearly how I can send a message that is stored in the history of the last 100 messages and broadcast to chat members (I know I need to use pid but all other stuff is unclear)

I made some type of pseudocode for ‘Message’ & ‘Room’ modules (and I need ‘User’ module too as I understand) but I have a big troubles with structs - how and where to define them correctly and how integrate them into my existing code
Message:

%Message{from: "username", message: "hello world"}

Room:

%Room{
  users: [%User{name: "user1", pid: PID }],
  history: [%Message{from: "username", message: "hello world"} ... ],
  name: "room name"
}

And some pseudocode callback

  # def handle_call(:chat_history, {FromPid, _Ref}, %Room{history: history, users: users} = room) do
  #   users
  #   |> Enum.map(&(&1.pid))
  #   |> then(fn pids -> FromPid in pids end)
  #   |> case do
  #      true ->  {:reply, history, room}
  #      false -> {:reply, {:error, "not a chat member"}, room}
  # end

Thx for help, this is my code:
server.ex

Summary
defmodule Chat.Server do
  use GenServer

  #Client
  def start_link() do
    GenServer.start_link(__MODULE__, [])
  end

  def send_message(pid, message) do # send_message
    GenServer.cast(pid, message)
  end

  def get_messages(pid) do # get list of messages ref to pid
    GenServer.call(pid, :get_messages) # :get_messages - 1st arg of handle_call func
  end

  def remove_message(pid, message) do
    GenServer.cast(pid, {:remove, message}) # :remove will pattern match with callback
  end

  def stop(pid) do
    GenServer.stop(pid, :normal, :infinity) # pid, shutdown reason(:normal - default), timeout(:infinity - default)
  end
  
  def init(list) do
    {:ok, list}
  end

  def terminate(_reason, list) do
    IO.puts("All messages are done")
    IO.inspect(list)
    :ok
  end

  def handle_cast({:remove, message}, list) do # pm ':remove' with tuple in remove_message
    updated_list = Enum.reject(list, fn(i) -> i == message end)
    {:noreply, updated_list}
  end

  def handle_cast(message, list) do
    updated_list = [message|list]
    {:noreply, updated_list}
  end

  def handle_call(:get_messages, _from, list) do # testing (atom, 2 elem tuple - colors PID, existing state of message list)
    {:reply, list, list}
  end
end

Marked As Solved

RudManusachi

RudManusachi

Hi, @liebus!
May be you could try to think of the behavior of the application:
For example:

  • messages are stored in the Chat.Server
  • users can read them from there
  • users can join the Chat.Server and then they will get “notified” about new messages
  • users could store a number of unread messages associated with a chat

Then model the interface first… (without implementation… just as in TDD)

For example:
Say Chat.Server holds the list of users and the list of messages.

{:ok, chat} = Chat.Server.start_link()   # chat is a pid of Chat.Server

{:ok, alice} = User.new(name: "Alice") # alice and bob are pids of Users
{:ok, bob} = User.new(name: "Bob")

:ok = Chat.Server.join(chat, alice)
:ok = Chat.Server.join(chat, bob)

# check the list of users
users = Chat.Server.list_users(chat)

# assure that both Alice and Bob are there
alice in users == true
bob in users == true

# send a message from Alice
Chat.Server.send_message(chat, "Hi, there", from: alice)

# check that Bob got an unread message
%{chat => 1} = User.unread_messages(bob)

So Chat.Server’s initial state might look like %{users: [], messages: []}
Chat.Server.join/2 would update that state adding user to the list.
every Chat.Server.send_message/3 will add message to the list and also broadcast a “notification” to all users, except the user that message came from.

User is also represented as a process that holds a map with chats and the number of unread messages. So that when it receives a notification from a Chat.Server it increments it.

BTW, it would be good if you convert that example to ExUnit test case :wink: and keep adding your testing examples and just run mix test to make sure it all works as expected =)

In more realistic scenario when multiple users interact with single Chat.Server fetching the list of users and the list of messages - you would want to move those lists to the DB that provides with concurrent access and keep Chat.Server as an interface to that DB. I think that might be a good example to learn a little bit of :ets :slightly_smiling_face:

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
Kagamiiiii
Student & New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

We're in Beta

About us Mission Statement