Slink - an AIM clone visually influenced by Windows 95
Slink started as a simple attempt at cloning the much-beloved team collaboration tool Slack, and quickly became an amalgamation of Slack and primordial AOL Instant Messenger.
- Web-based chat interface
- Leveraging of Rails' ActionCable for real-time message updates and channel broadcast management
- Multiple channels open for application event forwarding
- Unique channel creation
- Private chat with fellow users and SmarterChild
- SmarterChild bot running aboard Rails application
- Homegrown response generation algorithm
- Nostalgia instillation
- Windows 95 theme
- AOL Instant Messenger based interface and sound effects
- Backend
- Ruby on Rails
- Postgres
- Faker for db seeding
- Redis To Go for local ActionCable data storage
- Cloudinary for image hosting
- Heroku
- Frontend
- Tools
Original schema and controller design can be viewed in the docs folder of this repo. Naturally these designs went through several revisions during development.
The current state of the application persists data regarding users, messages & their relationship to users, and channels & their relationships to users & messages. A user can create public and private channels with other users, and remove or add their own subscriptions from the channel list interface.
Leveraging the use of Rails' ActiveRecord and router, controllers was be easily designed to provide the frontend software with api endpoints for data storage, retrieval, and processing.
Rails' ActionCable does the heavy lifting in creating the live-chat experience provided by Slink. By opening a socket channel for each chat window the client has open, users can have multiple chat streams running at one time, and the server can easily keep track of which users are subscribed to which channels in real time. By creating simple #sign_on!
and #sign_off!
methods on the Rails ApplicationController
, clients can be signed by their user_id
from the database:
# application_controller.rb
def sign_on!(user)
session[:session_token] = user.reset_session_token!
cookies.signed[:user_id] = user.id
end
def sign_off!
current_user.reset_session_token!
session[:session_token] = nil
cookies.delete :user_id
end
Hence, clients are tracked by that signed cookie:
# connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = User.find_by(id: cookies.signed[:user_id])
end
end
end
When a user subscribes to the ChatChannel
, they are simply added to a ActionCable channel which corresponds with the channel model they are subscribed to:
# chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
channel = Channel.find_by(id: params[:id])
stream_for channel
end
end
Users are notified of a new message in their channel through a post-then-broadcast pattern. When a user wants to send a message to a channel, a standard AJAX request is made to the message creation api endpoint through the Redux action sendMessage
:
// message_stream_window.jsx
handleSend(e) {
e.preventDefault();
this.sendMessage();
}
sendMessage() {
this.props.sendMessage(this.state.message).then(() => (
this.setState({ message: '' })
)).then(() => {
this.props.clearErrors();
return this.sendSound.play();
});
}
// message_stream_window.jsx
received: ({ message }) => {
if (this.props.currentUser.id !== message.authorId) {
this.receiveAudio.play();
}
this.props.receiveMessage(message);
this.messageInput.scrollTop = this.messageInput.scrollHeight;
}
When the message controller receives this message, it detects the message's corresponding channel and broadcasts it to all of the users currently subscribed to it:
# messages_controller.rb
if @message.save
ChatChannel.broadcast_to(channel, message: @message.camelized_json)
# ...
end
Additionally, by keeping track of user status via another channel (AppearanceChannel
), private message windows spawning was achieved. By subscribing each user additionally to the AppearanceChannel
, clients are notified of new private messages on private channels they are subscribed to, meaning an open window action can be launched at that time via the addChatWindow
action:
// buddy_list.jsx
received: channelId => {
if (!this.props.chatWindows.includes(channelId)) {
return this.props.requestChannel(channelId).then(
({ channel }) => {
this.props.addChatWindow(channel.id);
this.newPrivateMessageSound.play();
}
);
}
}
No AIM clone would be complete without some manifestation of SmarterChild. Hours of time and millions of billion were wasted conversing with this chatbot, who offered both bewildering retorts and surprisingly relevant witticisms.
The homegrown algorithm for generating Slink's SmarterChild responses is mounted on the messages model. By adding a self-joining id column to the messages table, a one-to-one relationship can be formed between messages, called reply
/prompt
. Not all messages have both a reply
and prompt
, but those that do offer an opportunity for using human chat interactions to influence SmarterChild's responses.
When SmarterChild receives a message, a similar prompt message is found in the database. SmarterChild then uses the reply to that prompt to respond to the original message.
- Limit initial message download count
- Channel subscription on a "room by room" basis (as provided by ActionCable)
- Message window flashing
- Real-time user status
- Filter messages
Arrows graphic by freepik from Flaticon is licensed under CC BY 3.0. Check out the new logo that I created on LogoMaker.com. https://logomakr.com/5kQFua5kQFua
Icons made by Freepik from Flaticon is licensed by CC 3.0 BY.
I would also like to thank all of my supportive classmates and instructors at App Academy.
And of course, most of all, thank you to my wonderful mother, Kathy Booth.