Skip to content
This repository has been archived by the owner on Sep 23, 2022. It is now read-only.
/ slink Public archive

This project has been re-written and migrated to https://github.com/broothie/slink.chat. An AIM clone visually influenced by Windows 95

Notifications You must be signed in to change notification settings

broothie/slink

Repository files navigation

Slink logo

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.

Features

  • 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

Implementation

Stack

Technical Challenges and Solutions

Backend Design

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.

Real-Time Chat

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

Real-Time Private Chat Spawning

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.

Future Improvements

Credits

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.