diff --git a/bindings/single.go b/bindings/single.go
index ca0e855cb2bdd53baa1da576ec198f2db5587976..a3bacbce28c6db129ae233ea0c684ad9b90a9817 100644
--- a/bindings/single.go
+++ b/bindings/single.go
@@ -166,7 +166,7 @@ type Stopper interface {
 // Parameters:
 //  - callbackReport - the JSON marshalled bytes of the SingleUseCallbackReport
 //    object, which can be passed into Cmix.WaitForRoundResult to see if the
-//    send succeeded.
+//    send operation succeeded.
 type SingleUseCallback interface {
 	Callback(callbackReport []byte, err error)
 }
@@ -177,7 +177,7 @@ type SingleUseCallback interface {
 // Parameters:
 //  - callbackReport - the JSON marshalled bytes of the SingleUseResponseReport
 //    object, which can be passed into Cmix.WaitForRoundResult to see if the
-//    send succeeded.
+//    send operation succeeded.
 type SingleUseResponse interface {
 	Callback(responseReport []byte, err error)
 }
diff --git a/bindings/ud.go b/bindings/ud.go
index 217fb964b7f4b5d313f323fca5df46d95cf9d577..3a3f21a0d43a4905ba5645d40cd38f4e82ed4854 100644
--- a/bindings/ud.go
+++ b/bindings/ud.go
@@ -429,8 +429,15 @@ func LookupUD(e2eID int, udContact []byte, cb UdLookupCallback,
 //
 // Parameters:
 //  - contactListJSON - the JSON marshalled bytes of []contact.Contact, or nil
-//    if an error occurs
-//  - err - any errors that occurred in the search
+//    if an error occurs.
+//
+//   JSON Example:
+//   {
+//  	"<xxc(2)F8dL9EC6gy+RMJuk3R+Au6eGExo02Wfio5cacjBcJRwDEgB7Ugdw/BAr6RkCABkWAFV1c2VybmFtZTA7c4LzV05sG+DMt+rFB0NIJg==xxc>",
+//  	"<xxc(2)eMhAi/pYkW5jCmvKE5ZaTglQb+fTo1D8NxVitr5CCFADEgB7Ugdw/BAr6RoCABkWAFV1c2VybmFtZTE7fElAa7z3IcrYrrkwNjMS2w==xxc>",
+//  	"<xxc(2)d7RJTu61Vy1lDThDMn8rYIiKSe1uXA/RCvvcIhq5Yg4DEgB7Ugdw/BAr6RsCABkWAFV1c2VybmFtZTI7N3XWrxIUpR29atpFMkcR6A==xxc>"
+//	}
+//  - err - any errors that occurred in the search.
 type UdSearchCallback interface {
 	Callback(contactListJSON []byte, err error)
 }
@@ -451,7 +458,7 @@ type UdSearchCallback interface {
 // Returns:
 //  - []byte - the JSON marshalled bytes of the SingleUseSendReport object,
 //    which can be passed into Cmix.WaitForRoundResult to see if the send
-//    succeeded.
+//    operation succeeded.
 func SearchUD(e2eID int, udContact []byte, cb UdSearchCallback,
 	factListJSON, singleRequestParamsJSON []byte) ([]byte, error) {
 
@@ -479,7 +486,20 @@ func SearchUD(e2eID int, udContact []byte, cb UdSearchCallback,
 	}
 
 	callback := func(contactList []contact.Contact, err error) {
-		contactListJSON, err2 := json.Marshal(contactList)
+		marshaledContactList := make([][]byte, 0)
+		// fixme: it may be wiser to change this callback interface
+		//   to simply do the work below when parsing the response from UD.
+		//   that would change ud/search.go in two places:
+		//    - searchCallback
+		//    - parseContacts
+		//  I avoid doing that as it changes interfaces w/o approval
+		for i := range contactList {
+			con := contactList[i]
+			marshaledContactList = append(
+				marshaledContactList, con.Marshal())
+		}
+
+		contactListJSON, err2 := json.Marshal(marshaledContactList)
 		if err2 != nil {
 			jww.FATAL.Panicf(
 				"Failed to marshal list of contact.Contact: %+v", err2)
diff --git a/channels/emoji.go b/channels/emoji.go
new file mode 100644
index 0000000000000000000000000000000000000000..d65844745e2c81ea549a962a81999fefc09e7214
--- /dev/null
+++ b/channels/emoji.go
@@ -0,0 +1,22 @@
+package channels
+
+import (
+	"github.com/forPelevin/gomoji"
+	"github.com/pkg/errors"
+)
+
+var InvalidReaction = errors.New("The reaction is not valid, " +
+	"it must be a single emoji")
+
+// ValidateReaction checks that the reaction only contains a single Emoji
+func ValidateReaction(reaction string) error {
+	if len(gomoji.RemoveEmojis(reaction)) > 0 {
+		return InvalidReaction
+	}
+
+	if len(gomoji.FindAll(reaction)) != 1 {
+		return InvalidReaction
+	}
+
+	return nil
+}
diff --git a/channels/eventModel.go b/channels/eventModel.go
index f86ec23e14ff8ca96574be95ddb65d3a5f03bc4c..93841ba39764e6fe9e608070f1fda363c781e0dc 100644
--- a/channels/eventModel.go
+++ b/channels/eventModel.go
@@ -2,6 +2,7 @@ package channels
 
 import (
 	"errors"
+	"github.com/golang/protobuf/proto"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
 	"gitlab.com/elixxir/primitives/states"
@@ -26,22 +27,23 @@ type EventModel interface {
 	LeaveChannel(ChannelID *id.ID)
 
 	ReceiveMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID,
-		SenderUsername string, Content []byte,
+		SenderUsername string, text string,
 		timestamp time.Time, lease time.Duration, round rounds.Round)
 
 	ReceiveReply(ChannelID *id.ID, MessageID cryptoChannel.MessageID,
 		ReplyTo cryptoChannel.MessageID, SenderUsername string,
-		Content []byte, timestamp time.Time, lease time.Duration,
+		text string, timestamp time.Time, lease time.Duration,
 		round rounds.Round)
 	ReceiveReaction(ChannelID *id.ID, MessageID cryptoChannel.MessageID,
 		ReactionTo cryptoChannel.MessageID, SenderUsername string,
-		Reaction []byte, timestamp time.Time, lease time.Duration,
+		Reaction string, timestamp time.Time, lease time.Duration,
 		round rounds.Round)
 
-	IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
-	UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
-	PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time)
-	UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	//unimplemented
+	//IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	//UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	//PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time)
+	//UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
 }
 
 type MessageTypeReceiveMessage func(ChannelID *id.ID,
@@ -63,8 +65,9 @@ func initEvents(model EventModel) *events {
 	}
 
 	//set up default message types
-	e.registered[Text] = e.model.ReceiveTextMessage
-	e.registered[AdminText] = e.model.ReceiveAdminTextMessage
+	e.registered[Text] = e.receiveTextMessage
+	e.registered[AdminText] = e.receiveTextMessage
+	e.registered[Reaction] = e.receiveReaction
 	return e
 }
 
@@ -127,3 +130,73 @@ func (e *events) triggerAdminEvent(chID *id.ID, cm *ChannelMessage,
 		cm.Payload, round.Timestamps[states.QUEUED], time.Duration(cm.Lease), round)
 	return
 }
+
+func (e *events) receiveTextMessage(ChannelID *id.ID,
+	MessageID cryptoChannel.MessageID, messageType MessageType,
+	SenderUsername string, Content []byte, timestamp time.Time,
+	lease time.Duration, round rounds.Round) {
+	txt := &CMIXChannelText{}
+	if err := proto.Unmarshal(Content, txt); err != nil {
+		jww.ERROR.Printf("Failed to text unmarshal message %s from %s on "+
+			"channel %s, type %s, ts: %s, lease: %s, round: %d: %+v",
+			MessageID, SenderUsername, ChannelID, messageType, timestamp, lease,
+			round.ID, err)
+		return
+	}
+
+	if txt.ReplyMessageID != nil {
+		if len(txt.ReplyMessageID) == cryptoChannel.MessageIDLen {
+			var replyTo cryptoChannel.MessageID
+			copy(replyTo[:], txt.ReplyMessageID)
+			e.model.ReceiveReply(ChannelID, MessageID, replyTo, SenderUsername, txt.Text,
+				timestamp, lease, round)
+			return
+
+		} else {
+			jww.ERROR.Printf("Failed process reply to for message %s from %s on "+
+				"channel %s, type %s, ts: %s, lease: %s, round: %d, returning "+
+				"without reply",
+				MessageID, SenderUsername, ChannelID, messageType, timestamp, lease,
+				round.ID)
+		}
+	}
+
+	e.model.ReceiveMessage(ChannelID, MessageID, SenderUsername, txt.Text,
+		timestamp, lease, round)
+}
+
+func (e *events) receiveReaction(ChannelID *id.ID,
+	MessageID cryptoChannel.MessageID, messageType MessageType,
+	SenderUsername string, Content []byte, timestamp time.Time,
+	lease time.Duration, round rounds.Round) {
+	react := &CMIXChannelReaction{}
+	if err := proto.Unmarshal(Content, react); err != nil {
+		jww.ERROR.Printf("Failed to text unmarshal message %s from %s on "+
+			"channel %s, type %s, ts: %s, lease: %s, round: %d: %+v",
+			MessageID, SenderUsername, ChannelID, messageType, timestamp, lease,
+			round.ID, err)
+		return
+	}
+
+	//check that the reaction is a single emoji and ignore if it isn't
+	if err := ValidateReaction(react.Reaction); err != nil {
+		jww.ERROR.Printf("Failed process reaction %s from %s on channel "+
+			"%s, type %s, ts: %s, lease: %s, round: %d, due to malformed "+
+			"reaction (%s), ignoring reaction",
+			MessageID, SenderUsername, ChannelID, messageType, timestamp, lease,
+			round.ID, err)
+	}
+
+	if react.ReactionMessageID != nil && len(react.ReactionMessageID) == cryptoChannel.MessageIDLen {
+		var reactTo cryptoChannel.MessageID
+		copy(reactTo[:], react.ReactionMessageID)
+		e.model.ReceiveReaction(ChannelID, MessageID, reactTo, SenderUsername,
+			react.Reaction, timestamp, lease, round)
+	} else {
+		jww.ERROR.Printf("Failed process reaction %s from %s on channel "+
+			"%s, type %s, ts: %s, lease: %s, round: %d, reacting to "+
+			"invalid message, ignoring reaction",
+			MessageID, SenderUsername, ChannelID, messageType, timestamp, lease,
+			round.ID)
+	}
+}
diff --git a/channels/interface.go b/channels/interface.go
index 8ecc115e96c73d7e8087e692a677fcc9fdcfd56d..f3a62ca7a49cb8c37b43393d2acf8824ab021a1a 100644
--- a/channels/interface.go
+++ b/channels/interface.go
@@ -27,7 +27,7 @@ type Manager interface {
 	SendReply(channelID *id.ID, msg string, replyTo cryptoChannel.MessageID,
 		validUntil time.Duration, params cmix.CMIXParams) (
 		cryptoChannel.MessageID, id.Round, ephemeral.Id, error)
-	SendReaction(channelID *id.ID, msg []byte,
+	SendReaction(channelID *id.ID, reaction string, reactTo cryptoChannel.MessageID,
 		validUntil time.Duration, params cmix.CMIXParams) (
 		cryptoChannel.MessageID, id.Round, ephemeral.Id, error)
 }
diff --git a/channels/joinedChannel.go b/channels/joinedChannel.go
index 0936b40eac894e6806d87ddc0faa77c230eafc42..0496298d6136b771a1cd53b2342ef180bafb4e2f 100644
--- a/channels/joinedChannel.go
+++ b/channels/joinedChannel.go
@@ -48,11 +48,14 @@ func (m *manager) storeUnsafe() error {
 
 // loadChannels loads all currently joined channels from disk and registers
 // them for message reception
-func (m *manager) loadChannels() map[*id.ID]*joinedChannel {
+func (m *manager) loadChannels() {
 
 	obj, err := m.kv.Get(joinedChannelsKey,
 		joinedChannelsVersion)
-	if err != nil {
+	if !m.kv.Exists(err) {
+		m.channels = make(map[*id.ID]*joinedChannel)
+		return
+	} else if err != nil {
 		jww.FATAL.Panicf("Failed to load channels %+v", err)
 	}
 
@@ -72,7 +75,8 @@ func (m *manager) loadChannels() map[*id.ID]*joinedChannel {
 		}
 		chMap[chList[i]] = jc
 	}
-	return chMap
+
+	m.channels = chMap
 }
 
 //addChannel Adds a channel
@@ -123,6 +127,22 @@ func (m *manager) addChannel(channel cryptoBroadcast.Channel) error {
 	return nil
 }
 
+func (m *manager) removeChannel(channelId *id.ID) error {
+	m.mux.Lock()
+	defer m.mux.Unlock()
+
+	ch, exists := m.channels[channelId]
+	if !exists {
+		return ChannelDoesNotExistsErr
+	}
+
+	ch.broadcast.Stop()
+
+	delete(m.channels, channelId)
+
+	return nil
+}
+
 //getChannel returns the given channel, if it exists
 func (m *manager) getChannel(channelId *id.ID) (*joinedChannel, error) {
 	m.mux.RLock()
diff --git a/channels/manager.go b/channels/manager.go
index 43ffa87b6de5ca4d574edac9064b906fce828bc2..ebad0194c14b4bece68862b8a26746e33620459e 100644
--- a/channels/manager.go
+++ b/channels/manager.go
@@ -1,22 +1,14 @@
 package channels
 
 import (
-	"github.com/golang/protobuf/proto"
 	"gitlab.com/elixxir/client/broadcast"
-	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/storage/versioned"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
-	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/fastRNG"
-	"gitlab.com/xx_network/crypto/signature/rsa"
 	"gitlab.com/xx_network/primitives/id"
-	"gitlab.com/xx_network/primitives/id/ephemeral"
 	"sync"
-	"time"
 )
 
-const cmixChannelTextVerion = 0
-
 type manager struct {
 	//List of all channels
 	channels map[*id.ID]*joinedChannel
@@ -30,195 +22,46 @@ type manager struct {
 
 	//Events model
 	events
-	broadcastMaker broadcast.NewBroadcastChannelFunc
-}
-
-func NewManager() {
-
-}
-
-func (m *manager) JoinChannel(channel cryptoBroadcast.Channel) error {
-	return m.addChannel(channel)
-}
-
-func (m *manager) SendGeneric(channelID *id.ID, msg []byte, validUntil time.Duration,
-	messageType MessageType, params cmix.CMIXParams) (cryptoChannel.MessageID,
-	id.Round, ephemeral.Id, error) {
-
-	//find the channel
-	ch, err := m.getChannel(channelID)
-	if err != nil {
-		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
-	}
-
-	var msgId cryptoChannel.MessageID
-	//Note: we are not checking check if message is too long before trying to
-	//find a round
-
-	//Build the function pointer that will build the message
-	assemble := func(rid id.Round) ([]byte, error) {
-
-		//Build the message
-		chMsg := &ChannelMessage{
-			Lease:       validUntil.Nanoseconds(),
-			RoundID:     uint64(rid),
-			PayloadType: uint32(messageType),
-			Payload:     msg,
-		}
-
-		//Serialize the message
-		chMsgSerial, err := proto.Marshal(chMsg)
-		if err != nil {
-			return nil, err
-		}
-
-		//Sign the message
-		messageSig, err := m.name.SignChannelMessage(chMsgSerial)
-		if err != nil {
-			return nil, err
-		}
-
-		//Build the user message
-		validationSig, unameLease := m.name.GetChannelValidationSignature()
-
-		usrMsg := &UserMessage{
-			Message:             chMsgSerial,
-			ValidationSignature: validationSig,
-			Signature:           messageSig,
-			Username:            m.name.GetUsername(),
-			ECCPublicKey:        m.name.GetChannelPubkey(),
-			UsernameLease:       unameLease.Unix(),
-		}
-
-		//Serialize the user message
-		usrMsgSerial, err := proto.Marshal(usrMsg)
-		if err != nil {
-			return nil, err
-		}
-
-		//Fill in any extra bits in the payload to ensure it is the right size
-		usrMsgSerialSized, err := broadcast.NewSizedBroadcast(
-			ch.broadcast.MaxAsymmetricPayloadSize(), usrMsgSerial)
-		if err != nil {
-			return nil, err
-		}
-
-		msgId = cryptoChannel.MakeMessageID(usrMsgSerialSized)
-
-		return usrMsgSerialSized, nil
-	}
 
-	//TODO: send the send message over to reception manually so it is added to
-	//the database early
-	rid, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params)
-	return msgId, rid, ephid, err
+	// make the function used to create broadcasts be a pointer so it
+	// can be replaced in tests
+	broadcastMaker broadcast.NewBroadcastChannelFunc
 }
 
-func (m *manager) SendAdminGeneric(privKey *rsa.PrivateKey, channelID *id.ID,
-	msg []byte, validUntil time.Duration, messageType MessageType,
-	params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id,
-	error) {
-
-	//find the channel
-	ch, err := m.getChannel(channelID)
-	if err != nil {
-		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
-	}
-
-	var msgId cryptoChannel.MessageID
-	//Note: we are not checking check if message is too long before trying to
-	//find a round
-
-	//Build the function pointer that will build the message
-	assemble := func(rid id.Round) ([]byte, error) {
-
-		//Build the message
-		chMsg := &ChannelMessage{
-			Lease:       validUntil.Nanoseconds(),
-			RoundID:     uint64(rid),
-			PayloadType: uint32(messageType),
-			Payload:     msg,
-		}
-
-		//Serialize the message
-		chMsgSerial, err := proto.Marshal(chMsg)
-		if err != nil {
-			return nil, err
-		}
-
-		//check if the message is too long
-		if len(chMsgSerial) > broadcast.MaxSizedBroadcastPayloadSize(privKey.Size()) {
-			return nil, MessageTooLongErr
-		}
-
-		//Fill in any extra bits in the payload to ensure it is the right size
-		chMsgSerialSized, err := broadcast.NewSizedBroadcast(
-			ch.broadcast.MaxAsymmetricPayloadSize(), chMsgSerial)
-		if err != nil {
-			return nil, err
-		}
-
-		msgId = cryptoChannel.MakeMessageID(chMsgSerialSized)
+func NewManager(kv *versioned.KV, client broadcast.Client,
+	rng *fastRNG.StreamGenerator, name NameService) Manager {
 
-		return chMsgSerialSized, nil
-	}
-
-	//TODO: send the send message over to reception manually so it is added to
-	//the database early
-	rid, ephid, err := ch.broadcast.BroadcastAsymmetricWithAssembler(privKey,
-		assemble, params)
-	return msgId, rid, ephid, err
-}
+	//prefix the kv with the username so multiple can be run
+	kv = kv.Prefix(name.GetUsername())
 
-func (m *manager) SendMessage(channelID *id.ID, msg string,
-	validUntil time.Duration, params cmix.CMIXParams) (
-	cryptoChannel.MessageID, id.Round, ephemeral.Id, error) {
-	txt := &CMIXChannelText{
-		Version:        cmixChannelTextVerion,
-		Text:           msg,
-		ReplyMessageID: nil,
+	m := manager{
+		kv:             kv,
+		client:         client,
+		rng:            rng,
+		name:           name,
+		events:         events{},
+		broadcastMaker: broadcast.NewBroadcastChannel,
 	}
 
-	txtMarshaled, err := proto.Marshal(txt)
-	if err != nil {
-		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
-	}
+	m.loadChannels()
 
-	return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params)
+	return &m
 }
 
-func (m *manager) SendReply(channelID *id.ID, msg string,
-	replyTo cryptoChannel.MessageID, validUntil time.Duration,
-	params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id,
-	error) {
-	txt := &CMIXChannelText{
-		Version:        cmixChannelTextVerion,
-		Text:           msg,
-		ReplyMessageID: replyTo[:],
-	}
-
-	txtMarshaled, err := proto.Marshal(txt)
+func (m *manager) JoinChannel(channel cryptoBroadcast.Channel) error {
+	err := m.addChannel(channel)
 	if err != nil {
-		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+		return err
 	}
-
-	return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params)
+	go m.events.model.JoinChannel(channel)
+	return nil
 }
 
-func (m *manager) SendReaction(channelID *id.ID, msg string,
-	replyTo cryptoChannel.MessageID, validUntil time.Duration,
-	params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id,
-	error) {
-	txt := &CMIXChannelText{
-		Version:        cmixChannelTextVerion,
-		Text:           msg,
-		ReplyMessageID: replyTo[:],
-	}
-
-	txtMarshaled, err := proto.Marshal(txt)
+func (m *manager) LeaveChannel(channelId *id.ID) error {
+	err := m.removeChannel(channelId)
 	if err != nil {
-		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+		return err
 	}
-
-	return m.SendGeneric(channelID, txtMarshaled, validUntil, Text, params)
+	go m.events.model.LeaveChannel(channelId)
+	return nil
 }
diff --git a/channels/send.go b/channels/send.go
index d72c017e84aa8688cf59f7a04353b0f96da67b06..86b34441aabd3e6a3d9a657cad2a177ae542b07d 100644
--- a/channels/send.go
+++ b/channels/send.go
@@ -1 +1,204 @@
 package channels
+
+import (
+	"gitlab.com/elixxir/client/broadcast"
+	"gitlab.com/elixxir/client/cmix"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/crypto/signature/rsa"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"google.golang.org/protobuf/proto"
+	"time"
+)
+
+const (
+	cmixChannelTextVersion     = 0
+	cmixChannelReactionVersion = 0
+)
+
+func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType,
+	msg []byte, validUntil time.Duration, params cmix.CMIXParams) (
+	cryptoChannel.MessageID, id.Round, ephemeral.Id, error) {
+
+	//find the channel
+	ch, err := m.getChannel(channelID)
+	if err != nil {
+		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+	}
+
+	var msgId cryptoChannel.MessageID
+	//Note: we are not checking check if message is too long before trying to
+	//find a round
+
+	//Build the function pointer that will build the message
+	assemble := func(rid id.Round) ([]byte, error) {
+
+		//Build the message
+		chMsg := &ChannelMessage{
+			Lease:       validUntil.Nanoseconds(),
+			RoundID:     uint64(rid),
+			PayloadType: uint32(messageType),
+			Payload:     msg,
+		}
+
+		//Serialize the message
+		chMsgSerial, err := proto.Marshal(chMsg)
+		if err != nil {
+			return nil, err
+		}
+
+		//Sign the message
+		messageSig, err := m.name.SignChannelMessage(chMsgSerial)
+		if err != nil {
+			return nil, err
+		}
+
+		//Build the user message
+		validationSig, unameLease := m.name.GetChannelValidationSignature()
+
+		usrMsg := &UserMessage{
+			Message:             chMsgSerial,
+			ValidationSignature: validationSig,
+			Signature:           messageSig,
+			Username:            m.name.GetUsername(),
+			ECCPublicKey:        m.name.GetChannelPubkey(),
+			UsernameLease:       unameLease.Unix(),
+		}
+
+		//Serialize the user message
+		usrMsgSerial, err := proto.Marshal(usrMsg)
+		if err != nil {
+			return nil, err
+		}
+
+		//Fill in any extra bits in the payload to ensure it is the right size
+		usrMsgSerialSized, err := broadcast.NewSizedBroadcast(
+			ch.broadcast.MaxAsymmetricPayloadSize(), usrMsgSerial)
+		if err != nil {
+			return nil, err
+		}
+
+		msgId = cryptoChannel.MakeMessageID(usrMsgSerialSized)
+
+		return usrMsgSerialSized, nil
+	}
+
+	//TODO: send the send message over to reception manually so it is added to
+	//the database early
+	rid, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params)
+	return msgId, rid, ephid, err
+}
+
+func (m *manager) SendAdminGeneric(privKey *rsa.PrivateKey, channelID *id.ID,
+	msg []byte, validUntil time.Duration, messageType MessageType,
+	params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id,
+	error) {
+
+	//find the channel
+	ch, err := m.getChannel(channelID)
+	if err != nil {
+		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+	}
+
+	var msgId cryptoChannel.MessageID
+	//Note: we are not checking check if message is too long before trying to
+	//find a round
+
+	//Build the function pointer that will build the message
+	assemble := func(rid id.Round) ([]byte, error) {
+
+		//Build the message
+		chMsg := &ChannelMessage{
+			Lease:       validUntil.Nanoseconds(),
+			RoundID:     uint64(rid),
+			PayloadType: uint32(messageType),
+			Payload:     msg,
+		}
+
+		//Serialize the message
+		chMsgSerial, err := proto.Marshal(chMsg)
+		if err != nil {
+			return nil, err
+		}
+
+		//check if the message is too long
+		if len(chMsgSerial) > broadcast.MaxSizedBroadcastPayloadSize(privKey.Size()) {
+			return nil, MessageTooLongErr
+		}
+
+		//Fill in any extra bits in the payload to ensure it is the right size
+		chMsgSerialSized, err := broadcast.NewSizedBroadcast(
+			ch.broadcast.MaxAsymmetricPayloadSize(), chMsgSerial)
+		if err != nil {
+			return nil, err
+		}
+
+		msgId = cryptoChannel.MakeMessageID(chMsgSerialSized)
+
+		return chMsgSerialSized, nil
+	}
+
+	//TODO: send the send message over to reception manually so it is added to
+	//the database early
+	rid, ephid, err := ch.broadcast.BroadcastAsymmetricWithAssembler(privKey,
+		assemble, params)
+	return msgId, rid, ephid, err
+}
+
+func (m *manager) SendMessage(channelID *id.ID, msg string,
+	validUntil time.Duration, params cmix.CMIXParams) (
+	cryptoChannel.MessageID, id.Round, ephemeral.Id, error) {
+	txt := &CMIXChannelText{
+		Version:        cmixChannelTextVersion,
+		Text:           msg,
+		ReplyMessageID: nil,
+	}
+
+	txtMarshaled, err := proto.Marshal(txt)
+	if err != nil {
+		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+	}
+
+	return m.SendGeneric(channelID, Text, txtMarshaled, validUntil, params)
+}
+
+func (m *manager) SendReply(channelID *id.ID, msg string,
+	replyTo cryptoChannel.MessageID, validUntil time.Duration,
+	params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id,
+	error) {
+	txt := &CMIXChannelText{
+		Version:        cmixChannelTextVersion,
+		Text:           msg,
+		ReplyMessageID: replyTo[:],
+	}
+
+	txtMarshaled, err := proto.Marshal(txt)
+	if err != nil {
+		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+	}
+
+	return m.SendGeneric(channelID, Text, txtMarshaled, validUntil, params)
+}
+
+func (m *manager) SendReaction(channelID *id.ID, reaction string,
+	replyTo cryptoChannel.MessageID, validUntil time.Duration,
+	params cmix.CMIXParams) (cryptoChannel.MessageID, id.Round, ephemeral.Id,
+	error) {
+
+	if err := ValidateReaction(reaction); err != nil {
+		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+	}
+
+	react := &CMIXChannelReaction{
+		Version:           cmixChannelReactionVersion,
+		Reaction:          reaction,
+		ReactionMessageID: replyTo[:],
+	}
+
+	reactMarshaled, err := proto.Marshal(react)
+	if err != nil {
+		return cryptoChannel.MessageID{}, 0, ephemeral.Id{}, err
+	}
+
+	return m.SendGeneric(channelID, Reaction, reactMarshaled, validUntil, params)
+}
diff --git a/channels/text.pb.go b/channels/text.pb.go
index ebada41edb208d4e5534f55a96831dedd407bd06..20dcb07ee9307c6d66aeaa3b5b5f118fc96d439b 100644
--- a/channels/text.pb.go
+++ b/channels/text.pb.go
@@ -96,7 +96,7 @@ type CMIXChannelReaction struct {
 	unknownFields protoimpl.UnknownFields
 
 	Version           uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
-	Reaction          uint32 `protobuf:"varint,2,opt,name=reaction,proto3" json:"reaction,omitempty"`
+	Reaction          string `protobuf:"bytes,2,opt,name=reaction,proto3" json:"reaction,omitempty"`
 	ReactionMessageID []byte `protobuf:"bytes,3,opt,name=reactionMessageID,proto3" json:"reactionMessageID,omitempty"`
 }
 
@@ -139,11 +139,11 @@ func (x *CMIXChannelReaction) GetVersion() uint32 {
 	return 0
 }
 
-func (x *CMIXChannelReaction) GetReaction() uint32 {
+func (x *CMIXChannelReaction) GetReaction() string {
 	if x != nil {
 		return x.Reaction
 	}
-	return 0
+	return ""
 }
 
 func (x *CMIXChannelReaction) GetReactionMessageID() []byte {
@@ -167,7 +167,7 @@ var file_text_proto_rawDesc = []byte{
 	0x43, 0x4d, 0x49, 0x58, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x61, 0x63, 0x74,
 	0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01,
 	0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a,
-	0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
+	0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
 	0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x11, 0x72, 0x65, 0x61,
 	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x18, 0x03,
 	0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65,
diff --git a/channels/text.proto b/channels/text.proto
index 4e1ad86791428f0d64e92b5bbb1d35d7b02567c4..c2d15310a23b0f293af9f3f2cad7e07332269290 100644
--- a/channels/text.proto
+++ b/channels/text.proto
@@ -18,6 +18,6 @@ message CMIXChannelText {
 
 message CMIXChannelReaction {
   uint32 version = 1;
-  uint32 reaction = 2;
+  string reaction = 2;
   bytes reactionMessageID = 3;
 }
\ No newline at end of file
diff --git a/go.mod b/go.mod
index e75c992bdbd5521e90da2f5193ab230c75279661..0056221fb2d42b9da6cb80e93398ff5cf7e764a0 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.17
 
 require (
 	github.com/cloudflare/circl v1.2.0
+	github.com/forPelevin/gomoji v1.1.6
 	github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
 	github.com/golang/protobuf v1.5.2
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
@@ -47,6 +48,7 @@ require (
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/rivo/uniseg v0.3.4 // indirect
 	github.com/rs/cors v1.7.0 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
diff --git a/go.sum b/go.sum
index 694614daf59cbc272114c5a9e4d535951e94677b..3be7615d55cf402389fc711faa2bde3ff66f280b 100644
--- a/go.sum
+++ b/go.sum
@@ -156,6 +156,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/forPelevin/gomoji v1.1.6 h1:mSIGhjyMiywuGFHR/6CLL/L6HwwDiQmYGdl1R9a/05w=
+github.com/forPelevin/gomoji v1.1.6/go.mod h1:h31zCiwG8nIto/c9RmijODA1xgN2JSvwKfU7l65xeTk=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -525,6 +527,8 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
+github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=