Initial commit

This commit is contained in:
2025-09-30 18:26:11 +02:00
commit fb57a41021
10 changed files with 395 additions and 0 deletions

3
.project Normal file
View File

@@ -0,0 +1,3 @@
{
'srcDirectory' : 'src'
}

3
src/.properties Normal file
View File

@@ -0,0 +1,3 @@
{
#format : #tonel
}

View File

@@ -0,0 +1,62 @@
Class {
#name : 'TCConsole',
#superclass : 'SpPresenter',
#instVars : [
'chat',
'list',
'input'
],
#category : 'TinyChat-client',
#package : 'TinyChat-client'
}
{ #category : 'as yet unclassified' }
TCConsole class >> attach: aTinyChat [
| window |
window := self new chat: aTinyChat.
window open whenClosedDo: [ aTinyChat disconnect ].
^ window
]
{ #category : 'as yet unclassified' }
TCConsole class >> defaultLayout [
^ SpBoxLayout newTopToBottom
add: #list; add: #input; yourself
]
{ #category : 'accessing' }
TCConsole >> chat: anObject [
chat := anObject
]
{ #category : 'accessing' }
TCConsole >> initializeWidgets [
list := SpListPresenter new.
input := SpTextInputFieldPresenter new
placeholder: 'Type your message here...';
enabled: true;
whenSubmitDo: [ :string | chat send: string. input text: '' ].
self focusOrder add: input
]
{ #category : 'accessing' }
TCConsole >> input [
^ input
]
{ #category : 'accessing' }
TCConsole >> list [
^ list
]
{ #category : 'accessing' }
TCConsole >> print: aCollectionOfMessages [
list items: (aCollectionOfMessages collect: [ :m | m printString ])
]
{ #category : 'accessing' }
TCConsole >> title [
^ 'TinyChat'
]

View File

@@ -0,0 +1,130 @@
Class {
#name : 'TinyChat',
#superclass : 'Object',
#instVars : [
'url',
'login',
'exit',
'messages',
'console',
'lastMessageIndex'
],
#category : 'TinyChat-client',
#package : 'TinyChat-client'
}
{ #category : 'as yet unclassified' }
TinyChat class >> connect: aHost port: aPort login: aLogin [
^ self new
host: aHost port: aPort login: aLogin;
start
]
{ #category : 'initialization' }
TinyChat >> cmdLastMessageID [
^ self command: '/messages/count'
]
{ #category : 'initialization' }
TinyChat >> cmdMessagesFromLastIndexToEnd [
"Returns the server messages from my current last index to the last on the server."
^ self command: '/messages' argument: lastMessageIndex
]
{ #category : 'initialization' }
TinyChat >> cmdNewMessage [
^self command: '/messages/add'
]
{ #category : 'initialization' }
TinyChat >> command: aPath [
^'{1}{2}' format: { url . aPath }
]
{ #category : 'initialization' }
TinyChat >> command: aPath argument: anArgument [
^'{1}{2}/{3}' format: { url . aPath . anArgument asString }
]
{ #category : 'initialization' }
TinyChat >> disconnect [
self sendNewMessage: (TCMessage from: login text: 'I exited from the chat room.').
exit := true
]
{ #category : 'as yet unclassified' }
TinyChat >> host: aHost port: aPort login: aLogin [
url := 'http://' , aHost , ':' , aPort asString.
login := aLogin
]
{ #category : 'initialization' }
TinyChat >> initialize [
super initialize.
exit := false.
lastMessageIndex := 0.
messages := OrderedCollection new
]
{ #category : 'initialization' }
TinyChat >> readLastMessageID [
| id |
id := (ZnClient new url: self cmdLastMessageID; get) asInteger.
id = 0 ifTrue: [ id := 1 ].
^ id
]
{ #category : 'initialization' }
TinyChat >> readMissingMessages [
"Gets the new messages that have been posted since the last request."
| response receivedMessages |
response := (ZnClient new url: self cmdMessagesFromLastIndexToEnd; get).
^ response
ifNil: [ 0 ]
ifNotNil: [
receivedMessages := response substrings: (String crlf).
receivedMessages do: [ :msg | messages add: (TCMessage fromString: msg) ].
receivedMessages size
]
]
{ #category : 'initialization' }
TinyChat >> refreshMessages [
[
[ exit ] whileFalse: [
(Delay forSeconds: 2) wait.
lastMessageIndex := lastMessageIndex + (self readMissingMessages).
console print: messages
]
] fork
]
{ #category : 'initialization' }
TinyChat >> send: aString [
"When we send a message, we push it to the server and in addition
we update the local list of posted messages"
| msg |
msg := TCMessage from: login text: aString.
self sendNewMessage: msg.
lastMessageIndex := lastMessageIndex + (self readMissingMessages).
console print: messages
]
{ #category : 'initialization' }
TinyChat >> sendNewMessage: aMessage [
^ ZnClient new
url: self cmdNewMessage;
formAt: 'sender' put: (aMessage sender);
formAt: 'text' put: (aMessage text);
post
]
{ #category : 'initialization' }
TinyChat >> start [
console := TCConsole attach: self.
self sendNewMessage: (TCMessage from: login text: 'I joined the chat room').
lastMessageIndex := self readLastMessageID.
self refreshMessages
]

View File

@@ -0,0 +1 @@
Package { #name : 'TinyChat-client' }

View File

@@ -0,0 +1,46 @@
Class {
#name : 'TCMessageQueue',
#superclass : 'Object',
#instVars : [
'messages'
],
#category : 'TinyChat-server',
#package : 'TinyChat-server'
}
{ #category : 'adding' }
TCMessageQueue >> add: aMessage [
messages add: aMessage
]
{ #category : 'adding' }
TCMessageQueue >> formattedMessagesFrom: aMessageNumber [
^ String streamContents: [ :formattedMessagesStream |
(self listFrom: aMessageNumber)
do: [ :m | formattedMessagesStream << m printString ]
]
]
{ #category : 'initialization' }
TCMessageQueue >> initialize [
super initialize.
messages := OrderedCollection new
]
{ #category : 'adding' }
TCMessageQueue >> listFrom: aIndex [
^ (aIndex > 0 and: [ aIndex <= messages size ])
ifTrue: [ messages copyFrom: aIndex to: messages size ]
ifFalse: [ #() ]
]
{ #category : 'adding' }
TCMessageQueue >> reset [
messages removeAll
]
{ #category : 'adding' }
TCMessageQueue >> size [
^ messages size
]

View File

@@ -0,0 +1,75 @@
Class {
#name : 'TCServer',
#superclass : 'Object',
#instVars : [
'teapotServer',
'messagesQueue'
],
#category : 'TinyChat-server',
#package : 'TinyChat-server'
}
{ #category : 'initialization' }
TCServer class >> startOn: aPortNumber [
^self new
initializePort: aPortNumber;
registerRoutes;
registerErrorHandlers;
yourself
]
{ #category : 'initialization' }
TCServer class >> stopAll [
self allInstancesDo: #stop
]
{ #category : 'initialization' }
TCServer >> addMessage: aRequest [
messagesQueue add: (TCMessage from: (aRequest at: #sender) text: (aRequest at: #text)).
]
{ #category : 'initialization' }
TCServer >> initialize [
super initialize.
messagesQueue := TCMessageQueue new
]
{ #category : 'initialization' }
TCServer >> initializePort: anInteger [
teapotServer := Teapot configure: {
#defaultOutput -> #text.
#port -> anInteger.
#debugMode -> true
}.
teapotServer start
]
{ #category : 'initialization' }
TCServer >> messageCount [
^ messagesQueue size
]
{ #category : 'initialization' }
TCServer >> messagesFrom: request [
^ messagesQueue formattedMessagesFrom: (request at: #id)
]
{ #category : 'initialization' }
TCServer >> registerErrorHandlers [
teapotServer
exception: KeyNotFound -> (TeaResponse notFound body: 'No such message')
]
{ #category : 'initialization' }
TCServer >> registerRoutes [
teapotServer
GET: '/messages/count' -> (Send message: #messageCount to: self);
GET: '/messages/<id:IsInteger>' -> (Send message: #messagesFrom: to: self);
POST: '/messages/add' -> (Send message: #addMessage: to: self)
]
{ #category : 'initialization' }
TCServer >> stop [
teapotServer stop.
messagesQueue reset
]

View File

@@ -0,0 +1 @@
Package { #name : 'TinyChat-server' }

View File

@@ -0,0 +1,73 @@
Class {
#name : 'TCMessage',
#superclass : 'Object',
#instVars : [
'sender',
'text',
'separator'
],
#category : 'TinyChat',
#package : 'TinyChat'
}
{ #category : 'instance creation' }
TCMessage class >> from: aSender text: aText [
^ self new sender: aSender; text: aText; yourself
]
{ #category : 'instance creation' }
TCMessage class >> fromString: aString [
^ self new
fromString: aString;
yourself
]
{ #category : 'initialization' }
TCMessage >> fromString: aString [
"Compose a message from a string of this form 'sender>message'."
| items |
items := aString substrings: separator.
self sender: items first.
self text: items second.
]
{ #category : 'initialization' }
TCMessage >> initialize [
super initialize.
separator := '>'
]
{ #category : 'printing' }
TCMessage >> printOn: aStream [
"Generate a string representation of the receiver based on its instance variables."
aStream
<< self sender; << separator;
<< self text; << String crlf
]
{ #category : 'accessing' }
TCMessage >> sender [
^ sender
]
{ #category : 'accessing' }
TCMessage >> sender: anObject [
sender := anObject
]
{ #category : 'accessing' }
TCMessage >> text [
^ text
]
{ #category : 'accessing' }
TCMessage >> text: anObject [
text := anObject
]

1
src/TinyChat/package.st Normal file
View File

@@ -0,0 +1 @@
Package { #name : 'TinyChat' }