
Teilen Sie:
Alex Lakatos ist ein JavaScript-Entwickler Advocate für Nexmo. In seiner Freizeit engagiert er sich bei Mozilla als Tech Speaker und Reps Mentor. Als JavaScript-Entwickler, der auf dem offenen Web aufbaut, verschiebt er jeden Tag dessen Grenzen. Wenn er nicht gerade in London programmiert, reist er gerne um die Welt, so dass man ihn wahrscheinlich in einer Flughafen-Lounge antrifft.
Erstellen einer Chat-Anwendung mit Angular Material und Vonage
Lesedauer: 26 Minuten
Hinweis: Einige der in diesem Artikel beschriebenen Tools oder Methoden werden möglicherweise nicht mehr unterstützt oder sind nicht mehr aktuell. Für aktualisierte Inhalte oder Support, überprüfen Sie unsere neuesten Beiträge oder kontaktieren Sie uns auf dem Vonage Community Slack
In diesem Tutorial aktivieren wir den Chat in einer Angular-Webanwendung mithilfe des JavaScript-SDK und der Conversation API damit die Benutzer in unserer Anwendung kommunizieren können. Wenn Sie sich den Quellcode ansehen möchten, finden Sie ihn auf unserer Vonage Community GitHub-Seite.
Das ist es, was wir versuchen, aufzubauen:

Vonage API-Konto
Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.
Bevor Sie beginnen
Bevor wir beginnen, benötigen Sie ein paar Dinge:
Ein grundlegendes Verständnis von Angular
Node.js auf Ihrem Rechner installiert haben.
Der Middleware-Code von GitHub.
Die Vonage CLI. Installieren Sie sie wie folgt:
npm install -g @vonage/cliRichten Sie die CLI so ein, dass sie Ihren Vonage-API-Schlüssel und Ihr API-Geheimnis verwendet:
vonage auth set –api-key=’VONAGE_API_KEY –api-secret=’VONAGE_API_SECRET’ Holen Sie sich den Middleware-Code von GitHub
Zuerst klonen wir den Middleware-Quellcode und installieren die Abhängigkeiten dafür. Wir werden eine Node.js-Anwendung mit Express schreiben, die eine Abstraktionsebene zwischen der Vonage-API und dem Angular-Code bietet:
git clone https://github.com/Nexmo/stitch-demo.git
cd stitch-demo
npm install Ausführen des Middleware-Codes von GitHub
Bevor wir den Code ausführen können, müssen wir eine RTC-Anwendung innerhalb der Vonage-Plattform erstellen, die in diesem Code verwendet wird:
Die Ausgabe des obigen Befehls sieht dann etwa so aus:
Das erste Element ist die Anwendungs-ID, die Sie sich notieren sollten (wir werden sie später als APP_ID später). Der letzte Wert ist der Standort des privaten Schlüssels. Der private Schlüssel wird verwendet, um JWTs zu erzeugen, die Ihre Interaktionen mit Vonage authentifizieren.
Jetzt müssen wir eine Kopie von example.env und rufen diese .envund aktualisieren Sie die Werte in Ihrem Vonage API_KEY und API_SECRET. Wir müssen auch den APP_ID Wir haben gerade den Pfad zu Ihrem privaten Schlüssel generiert. Nachdem wir die Werte aktualisiert haben, können wir den Code im Debug-Modus mit ausführen:
Benutzer und Konversationen erstellen
Die Anwendung sollte laufen auf localhost:3000. Jetzt, wo die App läuft, erstellen wir ein paar Benutzer und eine Konversation und fügen dann die erstellten Benutzer zur Konversation hinzu.
Wir erstellen ein paar Benutzer, indem wir diesen Befehl zweimal ausführen, einmal mit dem Benutzernamen alice und dann mit jamie:
Die Ausgabe sollte ungefähr so aussehen:
Wir notieren uns die Benutzer-ID und bezeichnen sie später als USER_ID. Lassen Sie uns nun eine Unterhaltung über die Demo-API erstellen:
Die Ausgabe sollte ungefähr so aussehen:
Wir notieren uns die Gesprächskennung und bezeichnen sie später als CONVERSATION_ID. Verbinden wir nun die Benutzer mit der Unterhaltung. Dazu führen wir den folgenden Befehl zweimal aus - denken Sie daran, die Zeichen CONVERSATION_ID und USER_ID durch die IDs aus den beiden vorangegangenen Schritten zu ersetzen, wenn Sie diesen Befehl ausführen:
Angular-App generieren
Nachdem wir nun unsere Middleware eingerichtet haben, ist es an der Zeit, die Angular-Anwendung zu erstellen. Wir werden die Angular CLI verwenden, um die Anwendung zu erstellen. Wenn Sie diese also nicht installiert haben, müssen Sie sie zuerst installieren:
Dann werden wir damit eine neue Anwendung mit Routing erstellen. Es kann eine Weile dauern, bis alle Dateien generiert und die Abhängigkeiten installiert sind:
Material Design hinzufügen
Nachdem der vorherige Befehl beendet ist, fügen wir Angular-Material und seine Abhängigkeiten zu dem Projekt hinzu:
Außerdem müssen wir das NgModule für jede Komponente, die wir in unserer Anwendung verwenden wollen, importieren. Um das zu tun, müssen wir src/app/app.module.ts in unserem Editor öffnen und die Module am oberen Rand importieren:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import {
MatAutocompleteModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatStepperModule
} from '@angular/material';Wir müssen auch die @NgModules imports-Deklaration aktualisieren, um die Module von oben hinzuzufügen:
@NgModule({
...
imports: [
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
MatTabsModule,
MatCardModule,
MatGridListModule,
MatButtonModule,
MatInputModule,
MatListModule,
MatIconModule,
MatSidenavModule,
MatProgressSpinnerModule,
MatTooltipModule,
MatDialogModule
],
],
...
})Wir wollen auch ein Thema zu Material hinzufügen, also fügen Sie diese Zeile zu Ihrem style.css:
@import "~@angular/material/prebuilt-themes/indigo-pink.css";Wenn wir die offiziellen Material Design-Symboleverwenden möchten, müssen wir die Icon-Schriftart in unser index.html:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> Polyfill Vonage Client SDK für JavaScript
Nachdem wir nun unsere Angular-Anwendung generiert und mit Material eingerichtet haben, installieren wir das Vonage Client SDK für JavaScript und fügen es am Ende von polyfills.ts:
/**********************************************************
* APPLICATION IMPORTS
*/
import 'nexmo-conversation'; Messaging-Dienst
Wir müssen einen Angular-Dienst erstellen, um die Daten von unserer Middleware zu verarbeiten. Erzeugen wir ihn mit der Angular CLI:
Zwei neue Dateien wurden in unserem src/app Ordner, messaging.service.spec.ts und messaging.service.spec.ts. Wir aktualisieren jetzt die messaging.service.spec.ts aktualisieren, um die ConversationClient aus dem Vonage In-App SDK hinzuzufügen, einen Client zu instanziieren und das Abrufen eines Benutzer JSON-Web-Token von der Middleware zu erhalten. Wir werden den Boilerplate-Code mit ersetzen:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
declare var ConversationClient: any;
const GATEWAY_URL = "http://localhost:3000/api/";
@Injectable()
export class MessagingService {
constructor(private http: HttpClient) {
}
initialize() {
this.client = new ConversationClient(
{
debug: false
}
)
}
public client: any
public app: any
public getUserJwt(username: string): Promise<any> {
return this.http.get(GATEWAY_URL + "jwt/" + username + "?admin=true").toPromise().then((response: any) => response.user_jwt)
}
}
Wir müssen die app.module.ts aktualisieren, um die MessagingService zu importieren und ihn als Anbieter zu registrieren:
import { MessagingService } from './messaging.service';
...
providers: [MessagingService],
... Login-Komponente
Beginnen wir mit der Erstellung einer Benutzeroberfläche für unsere App. Wir beginnen mit einer LoginComponent, die wir mit dem Angular CLI generieren:
Dadurch wird ein login Ordner innerhalb des app und vier Dateien für den HTML-, CSS- und TypeScript-Code sowie für die Tests. Ersetzen wir den Code in login.component.html durch eine Benutzeroberfläche für die Anmeldung. Ich wählte eine <mat-grid-list> mit einem <mat-card> und ein Formular mit einer Anmeldeschaltfläche, die onLogin() aufruft, wenn es abgeschickt wird. Der Code sieht wie folgt aus:
<mat-grid-list cols="4" rowHeight="100px">
<mat-grid-tile colspan="1" rowspan="5"></mat-grid-tile>
<mat-grid-tile colspan="2" rowspan="1"></mat-grid-tile>
<mat-grid-tile colspan="1" rowspan="5"></mat-grid-tile>
<mat-grid-tile colspan="2" rowspan="3">
<mat-card class="mat-typography login">
<h1>Login</h1>
<form (ngSubmit)="onLogin()">
<mat-form-field class="full-width">
<input matInput placeholder="Username" name="username" [(ngModel)]="username">
</mat-form-field>
<br>
<button type="submit" mat-raised-button color="primary" (click)="showSpinner = !showSpinner">Login
<mat-spinner color="accent" mode="indeterminate" *ngIf="showSpinner"></mat-spinner>
</button>
</form>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
Fügen wir etwas CSS für den Material-Spinner in der login.component.css Datei:
.login {
text-align: center;
}
.mat-spinner {
width: 20px !important;
height: 20px !important;
display: inline-block;
margin-left: 10px;
}
/deep/ .mat-spinner svg{
width: 20px !important;
height: 20px !important;
}Wir müssen auch die login.component.ts aktualisieren, um die MessagingService hinzuzufügen und die onLogin() Methode zu implementieren. Die Methode nimmt den Benutzernamen, stellt eine Anfrage über den Messaging Service an die Middleware, die wir ausführen, um ein Benutzer-JWT zu erhalten, und verwendet dieses dann zur Authentifizierung über die login Methode des Vonage In-App JavaScript SDK zu authentifizieren. Der Code sieht wie folgt aus:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { MessagingService } from '../messaging.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
username: string = ""
constructor(private ms: MessagingService, private router: Router) { }
ngOnInit() {
this.ms.initialize()
}
onLogin() {
this.ms.getUserJwt(this.username).then(this.authenticate.bind(this))
}
authenticate(userJwt: string) {
this.ms.client.login(userJwt).then(app => {
this.ms.app = app
this.router.navigate(['/conversation']);
})
}
}
Beachten Sie, dass ich die Router aus Angular importiert habe, es in unseren Konstruktor injiziert habe und es am Ende des Authentifizierungsflusses verwende, um zur nächsten Seite zu navigieren, /conversation.
Komponente Konversation
Wir haben noch nicht wirklich die conversation Komponente noch nicht erstellt, also verwenden wir die Angular CLI, um sie zu erstellen:
Wir werden die Datei conversation.component.html Datei aktualisieren, um ein Material zu verwenden sidenav Komponente die Benutzergespräche auf der linken Seite und die Gesprächsteilnehmer auf der rechten Seite auflistet und die Mitte für unseren Hauptchat freilässt. Wir fügen dem Chat-Abschnitt eine Kopfzeile hinzu, um den Namen der Konversation und die Anzahl der Mitglieder aufzulisten, und am unteren Rand wird ein Eingabebereich hinzugefügt. Der mittlere Bereich wird für die Anzeige des eigentlichen Gesprächsverlaufs reserviert. Wir erstellen zunächst eine leere Shell und fügen sie später hinzu, wenn wir die ConversationComponent. Die HMTL sollte wie folgt aussehen:
<mat-sidenav-container class="container">
<mat-sidenav mode="side" opened>
<mat-card>
<mat-tab-group>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon matListIcon>forum</mat-icon>
</ng-template>
<mat-list class="conversations">
...
</mat-list>
</mat-tab>
</mat-tab-group>
</mat-card>
</mat-sidenav>
<mat-sidenav position="end" mode="side" opened *ngIf="selectedConversation">
<mat-card>
<mat-list class="members">
...
</mat-list>
</mat-card>
</mat-sidenav>
<section class="empty-conversation" *ngIf="!selectedConversation">
<h1 class="mat-display-1">Select a conversation from the left to start chatting</h1>
</section>
<section *ngIf="selectedConversation">
<div class="mat-typography conversation-header">
...
</div>
<mat-divider></mat-divider>
<mat-list dense class="conversation-history mat-typography">
...
</mat-list>
<div class="conversation-input">
<mat-divider></mat-divider>
<mat-form-field class="full-width">
<input matInput placeholder="Start chatting..." name="text" [(ngModel)]="text">
<mat-icon matSuffix (click)="">send</mat-icon>
</mat-form-field>
</div>
</section>
</mat-sidenav-container>
Wir werden die Datei conversation.component.ts Datei mit dem notwendigen Boilerplate für Methoden, die wir später verwenden werden:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';
import { MessagingService } from '../messaging.service';
@Component({
selector: 'app-conversation',
templateUrl: './conversation.component.html',
styleUrls: ['./conversation.component.css']
})
export class ConversationComponent implements OnInit {
constructor(private ms: MessagingService, private router: Router) { }
buildConversationsArray(conversations) {
}
ngOnInit() {
}
selectConversation(conversationId: string) {
}
sendText(text: string) {
}
conversations: any
selectedConversation: any
text: string
events: Array<any> = []
}
Beginnen wir mit der Implementierung von ngOnInit() damit es überprüft, ob wir App-Daten haben, bevor wir versuchen getConversations das In-App-SDK zu verwenden. Wenn keine App-Daten vorhanden sind, werden wir zum Anmeldebildschirm weitergeleitet:
ngOnInit() {
if (!this.ms.app) {
this.router.navigate(['/']);
} else {
this.ms.app.getConversations().then(conversations => {
this.conversations = this.buildConversationsArray(conversations)
})
}
}
Wir müssen eine Hilfsmethode implementieren, um ein Konversations-Array aus dem Konversations-Wörterbuch zu erstellen, das das In-App JavaScript SDK bereitstellt, damit wir es später mit *ngFor später in der Benutzeroberfläche verwenden können:
buildConversationsArray(conversations) {
let array = [];
for (let conversation in conversations) {
array.push(conversations[conversation]);
}
return array
}Fügen wir eine Methode zur Auswahl eines Gesprächs aus der Liste hinzu. Wir müssen die ID aus der Ansicht übernehmen, sie an den Controller weitergeben und dann das Vonage SDK verwenden, um Daten über das Gespräch zu erhalten. Wir speichern diese Daten in einer Klasseneigenschaft, damit sie später in der Ansicht verfügbar sind. Wir verwenden auch Observable um ein Array aus der conversation.events Map, damit wir den Chatverlauf wiederherstellen können, wenn der Benutzer zur App zurückkehrt. Wir fügen auch einen Ereignis-Listener mit dem SDK hinzu, um auf text Ereignisse und fügen diese dem Ereignisverlauf hinzu:
selectConversation(conversationId: string) {
this.ms.app.getConversation(conversationId).then(conversation => {
this.selectedConversation = conversation
Observable.from(conversation.events.values()).subscribe(
event => {
this.events.push(event)
}
)
this.selectedConversation.on("text", (sender, message) => {
this.events.push(message)
})
console.log("Selected Conversation", this.selectedConversation)
}
)
}
Zu guter Letzt fügen wir eine Methode hinzu, die die Eingaben aus der Ansicht nimmt und sie über das SDK an die Vonage In-App API sendet:
sendText(text: string) {
this.selectedConversation.sendText(text).then(() => this.text = "")
}
Nun, da wir alle benötigten Methoden implementiert haben, können wir zurückgehen und die Ansicht weiter ausbauen, um die Datenmodelle zu verwenden, die wir im Controller erstellt haben. Zuerst aktualisieren wir den conversations Abschnitt in conversation.component.html:
...
<mat-list class="conversations">
<mat-list-item *ngFor="let conversation of conversations" (click)="selectConversation(conversation.id)">
<mat-icon matListIcon>forum</mat-icon>
<p>{{conversation.display_name}}</p>
</mat-list-item>
</mat-list>
...
Fügen wir nun den Bereich Mitglieder hinzu:
...
<mat-list class="members">
<mat-list-item *ngFor="let member of selectedConversation.members | keys">
<p>{{member.value.user.name}}</p>
</mat-list-item>
</mat-list>
...
Wir verwenden eine Pipe namens keys hier, um das members Dictionary-Objekt, das wir vom SDK erhalten, in ein Array umzuwandeln, also müssen wir es mit dem Angular CLI erstellen und die generierte keys.pipe.ts Datei aktualisieren:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'keys'
})
export class KeysPipe implements PipeTransform {
transform(value, args:string[]) : any {
let keys = [];
for (let key in value) {
keys.push({key: key, value: value[key]});
}
return keys;
}
}Als nächstes aktualisieren wir den conversation-header Abschnitt der Ansicht, um den Namen der ausgewählten Unterhaltung und die Anzahl der Mitglieder anzuzeigen:
...
<div class="mat-typography conversation-header">
<h2>
<mat-icon>forum</mat-icon>
{{selectedConversation.display_name}}</h2>
<p>
<mat-icon>account_circle</mat-icon>
{{(selectedConversation.members | keys).length}} Members</p>
</div>
...Wir müssen auch den conversation-history Abschnitt aktualisieren, um die Ereignisse zu analysieren und den Verlauf im Chat wiederherzustellen. Ereignisse, die aus dem In-App SDK kommen, haben mehrere Typen, also werden wir einige davon berücksichtigen, wie member:joined und text:
...
<mat-list dense class="conversation-history mat-typography">
<mat-list-item *ngFor="let event of events; index as i" [dir]="event.from === selectedConversation.me.id ? 'rtl' : 'ltr'">
<img *ngIf="event.type == 'text'" matListAvatar matTooltip="{{selectedConversation.members[event.from].user.name}}" src="https://randomuser.me/api/portraits/thumb/lego/{{i}}.jpg"
/>
<p *ngIf="event.type == 'text'" [dir]="'ltr'">{{event.body.text}}</p>
<p *ngIf="event.type == 'member:joined'" class="text-center">
<b>{{selectedConversation.members[event.from].user.name}}</b> has joined the conversation</p>
</mat-list-item>
</mat-list>
...
Wir müssen den conversation-input Teil aktualisieren, um Nachrichten an die Konversation senden zu können:
...
<div class="conversation-input">
<mat-divider></mat-divider>
<mat-form-field class="full-width">
<input matInput placeholder="Start chatting..." name="text" [(ngModel)]="text">
<mat-icon matSuffix (click)="sendText(text)">send</mat-icon>
</mat-form-field>
</div>
...
Fügen wir ein paar CSS hinzu, um es bildschirmfüllend und in einer festen Position anzuzeigen. Fügen Sie das folgende CSS zu conversation.component.css:
.container {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.mat-drawer.mat-drawer-side {
padding: 0 5px;
}
.empty-conversation {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.conversation-header h2, .conversation-header p {
align-items: center;
display: flex;
}
.text-center {
text-align: center;
width: 100%;
}
.conversation-history.mat-list {
height: calc(100% - 180px);
overflow-x: scroll;
position: absolute;
width: 100%;
}
.conversation-history.mat-list p {
margin: 0;
}
.empty-conversation h1 {
margin: 0;
}
.conversations .mat-list-item {
cursor: pointer;
}
.mat-card {
height: 100%;
padding: 0 24px;
overflow: scroll;
}
.conversation-input {
position: absolute;
bottom: 0;
width: 100%;
background-color: #fafafa;
}
section .mat-list .mat-list-avatar{
width: 25px;
height: 25px;
}
.mat-list-avatar {
margin: 0 5px;
}
.right {
text-align: right;
}
.full-width {
width: 100%;
}
.full-width .mat-icon {
cursor: pointer;
}Zu guter Letzt müssen wir das App-Routing-Modul in app-routing.module.ts aktualisieren, damit die richtigen Routen angezeigt werden:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ConversationComponent } from './conversation/conversation.component';
const routes: Routes = [
{
path: '',
component: LoginComponent,
},
{
path: 'conversation',
component: ConversationComponent,
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [
RouterModule
],
declarations: []
})
export class AppRoutingModule { }Wir müssen auch die gesamte app.component.html durch das <router-outlet> ersetzen, um den Router auf der ersten Seite anzuzeigen:
<router-outlet></router-outlet> Führen Sie Ihre App aus!
Nachdem Sie die in diesem Beitrag beschriebene Anwendung erstellt haben, führen Sie sie aus, um zu sehen, ob sie funktioniert:
Die App wird unter "http://localhost:4200" ausgeführt. Ich würde vorschlagen, die App in zwei separaten Tabs zu öffnen, sich mit beiden anzumelden alice und jamie und fangen Sie an, miteinander zu reden! Wenn Sie die App in ihrem endgültigen Zustand sehen möchten, können Sie den Quellcode für diese App auf unserer GitHub-Seite der Gemeinschaft. Wenn Sie eine fortgeschrittenere Version dieses Codes sehen möchten, können Sie den stitch-demo Middleware-Code, den Sie zu Beginn des Blogbeitrags heruntergeladen haben. Er enthält auch ein Angular Material Frontend.
Was kommt als Nächstes?
Wenn Sie die Verwendung des Vonage Client SDK für JavaScript weiter erlernen möchten, schauen Sie sich unseren Quickstart an, in dem wir Ihnen zeigen, wie es geht:
mehr Ereignis-Listener verwenden um den Chatverlauf anzuzeigen, und wenn ein Benutzer tippt
Haben Sie eine Frage oder möchten Sie etwas mitteilen? Beteiligen Sie sich am Gespräch auf dem Vonage Community Slackund bleiben Sie auf dem Laufenden mit dem Entwickler-Newsletter, folgen Sie uns auf X (früher Twitter), abonnieren Sie unseren YouTube-Kanal für Video-Tutorials, und folgen Sie der Vonage Entwickler-Seite auf LinkedInein Raum für Entwickler, um zu lernen und sich mit der Community zu vernetzen. Bleiben Sie in Verbindung, teilen Sie Ihre Fortschritte und halten Sie sich über die neuesten Nachrichten, Tipps und Veranstaltungen für Entwickler auf dem Laufenden!
Teilen Sie:
Alex Lakatos ist ein JavaScript-Entwickler Advocate für Nexmo. In seiner Freizeit engagiert er sich bei Mozilla als Tech Speaker und Reps Mentor. Als JavaScript-Entwickler, der auf dem offenen Web aufbaut, verschiebt er jeden Tag dessen Grenzen. Wenn er nicht gerade in London programmiert, reist er gerne um die Welt, so dass man ihn wahrscheinlich in einer Flughafen-Lounge antrifft.
